From b258215c9e6cbf28446103e023853772a4c337e8 Mon Sep 17 00:00:00 2001 From: ErickHub192 Date: Thu, 27 Aug 2026 10:30:55 -0600 Subject: [PATCH] La sala nace vacia de verdad Co-Authored-By: Claude Opus 5 --- server/src/agent/loop.ts | 12 +++------ server/src/demos/variables.ts | 10 +++++++- server/src/engine/git.ts | 6 ++--- server/src/engine/workspace.ts | 46 +++++++++++++++++++++++++--------- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/server/src/agent/loop.ts b/server/src/agent/loop.ts index bcc983c..0ed06c7 100644 --- a/server/src/agent/loop.ts +++ b/server/src/agent/loop.ts @@ -73,13 +73,9 @@ créalo con bash: es tu trabajo, no preguntes por dónde empezar. El orden importa porque hasta que no hay manifiesto no hay instalación, sin instalación no hay dev server, y sin dev server la sala mira una pantalla vacía. - El proyecto va EN LA RAÍZ del directorio de trabajo, no en una subcarpeta. - Pero la raíz nunca está vacía: Multi siembra ahí su .gitignore antes de que llegues. - Un generador que se planta a preguntar si continúa sobre un directorio con archivos - se cancela solo, porque aquí nadie le contesta, y no hay bandera que lo arregle. - Cuando eso pase, no insistas: dale el directorio vacío que espera y mueve después a - la raíz lo que haya dejado. - Prefiere eso a escribir la configuración del stack de memoria, porque la plantilla - oficial está al día y lo que tú recuerdas es de cuando te entrenaron. + La raíz está vacía, así que los generadores del stack corren ahí sin problema. + Prefiérelos a escribir la configuración de memoria: su plantilla + está al día y lo que tú recuerdas es de cuando te entrenaron. Comprueba que el manifiesto quedó en la raíz antes de seguir. - Si te dicen el stack, usa ese, sea cual sea (Next, Svelte, Django, Go, lo que pidan). - Si no te lo dicen, elige uno moderno y sensato en vez de interrogar a alguien que @@ -132,7 +128,7 @@ créalo con bash: es tu trabajo, no preguntes por dónde empezar. Pide las credenciales de una base externa por el panel de Variables, diciendo los nombres exactos que vas a leer, y ofrece dejar la app andando con datos de prueba mientras llegan. - El archivo de una base local NO entra al historial (está en el .gitignore): lo de + El archivo de una base local NO entra al historial (Multi ya lo ignora): lo de ahí son datos de prueba. Deja el esquema en el código o en una migración, para que la app arranque sola en una base vacía. Y dilo al cerrar, en una línea: una base local vive SOLO en esta sala y no viaja diff --git a/server/src/demos/variables.ts b/server/src/demos/variables.ts index 3f7a00e..68828e6 100644 --- a/server/src/demos/variables.ts +++ b/server/src/demos/variables.ts @@ -1,4 +1,5 @@ import { readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; import { join } from "node:path"; import { createWorkspace } from "../engine/workspace.js"; import { guardarVariables, leerVariables } from "../engine/env.js"; @@ -71,9 +72,16 @@ async function main() { check("las anteriores se fueron", v3.length === 1, v3.map((v) => v.nombre).join(",")); console.log("\n6. Y no entra al historial de la sala"); - const gitignore = await readFile(join(ws.dir, ".gitignore"), "utf8"); + // Las reglas del motor viven en `.git/info/exclude`, no en la raíz: así la + // sala nace vacía y ningún generador de proyecto las pisa. + const gitignore = await readFile(join(ws.dir, ".git", "info", "exclude"), "utf8"); check("el .env está ignorado", gitignore.includes(".env")); + // Y la sala nace VACÍA: un .gitignore en la raíz haría que los generadores de + // proyecto se planten a preguntar, y que el primero en correr lo pise con el + // suyo. Las dos cosas pasaron antes de mover las reglas a .git/info/exclude. + check("no hay .gitignore en la raíz", !existsSync(join(ws.dir, ".gitignore"))); + console.log(`\n${pass} pasaron, ${fail} fallaron\n`); process.exit(fail > 0 ? 1 : 0); } diff --git a/server/src/engine/git.ts b/server/src/engine/git.ts index 1ca4163..e5e8ee1 100644 --- a/server/src/engine/git.ts +++ b/server/src/engine/git.ts @@ -228,9 +228,9 @@ export async function hayCommits(dir: string): Promise { * La diferencia no es teórica: un agente que crea archivos nuevos (el caso * normal de un turno a medias) no se detectaba con `diff-index`. * - * Sin ningún commit todavía se responde `false`: la sala recién nacida solo - * tiene el `.gitignore` del motor sin trackear, y llamar a eso "trabajo sin - * guardar" sería avisar de algo que nadie escribió. + * Sin ningún commit todavía se responde `false`: la sala recién nacida está + * vacía, y llamar a eso "trabajo sin guardar" sería avisar de algo que nadie + * escribió. */ export async function tieneCambiosSinCommitear(dir: string): Promise { if (!(await hayCommits(dir))) return false; diff --git a/server/src/engine/workspace.ts b/server/src/engine/workspace.ts index 5b702af..6a8567b 100644 --- a/server/src/engine/workspace.ts +++ b/server/src/engine/workspace.ts @@ -1,6 +1,6 @@ -import { mkdir, writeFile, rm } from "node:fs/promises"; +import { mkdir, writeFile, readFile, rm } from "node:fs/promises"; import { existsSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; @@ -24,8 +24,10 @@ export interface Workspace { * proyecto de arranque sería meter una plantilla por la puerta de atrás y dejar * de ser agnósticos (era andamio de la Fase 0 para probar el motor; ya no). * - * Lo único que se siembra es `.gitignore`, y eso SÍ es del motor: sin él, - * `git add -A` indexa node_modules y los turnos nunca commitean. + * Vacía de verdad, ni un archivo. Lo que el motor necesita ignorar vive dentro + * de `.git`, fuera de la vista: si estuviera en la raíz, los generadores de + * proyecto se plantarían a preguntar si continúan sobre un directorio con + * archivos, y el primero en correr lo pisaría con el suyo. */ export async function createWorkspace( roomId: string, @@ -57,17 +59,33 @@ export async function createWorkspace( } /** - * Siembra el `.gitignore` del motor si no existe. + * Las reglas de ignorar del motor, en `.git/info/exclude`. * - * No es plantilla de proyecto: es la condición para que el historial funcione. - * Si el agente después lo edita para su stack, se respeta (por eso no se pisa). + * NO en un `.gitignore` de la raíz, y esa es la decisión que importa: git lee + * los dos igual, pero este vive dentro de `.git` y por lo tanto la carpeta de + * trabajo queda VACÍA. + * + * Con un archivo en la raíz, un generador (`npm create`, `create-next-app`…) se + * planta a preguntar si continúa sobre un directorio que no está vacío, nadie le + * contesta y se cancela; y el que sí corre escribe su propio `.gitignore` + * encima, borrando lo del motor sin que nadie se entere. Las dos cosas pasaron + * de verdad, y le costaban al agente media docena de comandos por sala. + * + * Nada de esto es plantilla de proyecto: es la condición para que el historial + * funcione. Si el proyecto trae su propio `.gitignore`, ese es suyo y se queda. */ +const MARCA = "# Lo que nunca entra al historial de la sala. Lo pone Multi."; + async function ensureGitignore(dir: string): Promise { - const path = join(dir, ".gitignore"); - if (existsSync(path)) return; + const path = join(dir, ".git", "info", "exclude"); + // `git init` ya deja aquí un archivo con instrucciones comentadas, así que no + // vale preguntar si EXISTE: hay que mirar si son NUESTRAS reglas las que están. + // Saltárselo dejaba el .env sin ignorar, y en silencio. + const previo = existsSync(path) ? await readFile(path, "utf8") : ""; + if (previo.includes(MARCA)) return; const lineas = [ - "# Lo que nunca entra al historial de la sala.", - "# El agente puede agregar lo que su stack necesite.", + MARCA, + "# El proyecto puede tener su propio .gitignore para lo suyo.", "node_modules/", "dist/", "build/", @@ -100,7 +118,11 @@ async function ensureGitignore(dir: string): Promise { "*.db-wal", "", ]; - await writeFile(path, lineas.join("\n"), "utf8"); + // Se AÑADE a lo que `git init` dejó, en vez de pisarlo: eso son comentarios de + // ayuda de git y borrarlos no aporta nada. + const separador = previo && !previo.endsWith("\n") ? "\n" : ""; + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, previo + separador + lineas.join("\n"), "utf8"); } /**