Forzar máximo 2 steps en plan: 1 coder + 1 reviewer opcional

El planner generaba 3+ steps para tareas simples causando que el
coder repitiera acciones en cada step (creaba el módulo varias veces).
Ahora el engine fusiona los steps en 1 coder con descripción combinada.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jordan Diaz
2026-04-03 23:47:05 +00:00
parent ded0e997ed
commit 301cef4d69
8 changed files with 267 additions and 182 deletions

View File

@@ -4,6 +4,13 @@
El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas). El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas).
Reglas obligatorias:
- Todo elemento editable con `data-field-type` debe incluir también `data-field-label`
- Si falta `data-field-label`, el builder puede generar variables temporales o incorrectas y el módulo queda mal configurado
- Usa labels descriptivos y estables; no dejes labels vacíos ni genéricos como "Campo" o "Texto"
Evita también en `index-base.tpl` las clases Tailwind con valores arbitrarios como `text-[44px]`, `font-['Cinzel']` o `leading-[1.1]`. En este stack pueden romper el parseo/compilación del template. Muévelas a `style.css`.
| Label | Variable | | Label | Variable |
|-------|----------| |-------|----------|
| Categoría Noticia | `categoranoticia` | | Categoría Noticia | `categoranoticia` |

View File

@@ -72,12 +72,29 @@ Patrón para colores configurables por el usuario:
JavaScript scopeado al módulo usando `section_id`: JavaScript scopeado al módulo usando `section_id`:
This is the default and expected place for module JavaScript.
Do NOT embed `<script>` tags directly inside `index-base.tpl` for normal module behavior.
Keep `index-base.tpl` for HTML/Twig markup and put interactive logic in `script.js`.
`script.js` is a static file: do NOT use Twig syntax inside it.
Do NOT write `{{ section_id }}`, `{{ variable }}`, `{% if ... %}`, or builder attributes inside `script.js`.
If JavaScript needs dynamic values, pass them from `index-base.tpl` through `data-*` attributes:
```html
<section
class="buscador-apartados"
data-section-id="{{ section_id }}"
data-hook-endpoint="/hooks/buscadorapartados_hjd8s/">
</section>
```
```js ```js
const section = document.getElementById('{{ section_id }}'); document.querySelectorAll('.buscador-apartados').forEach((section) => {
if (section) { const sectionId = section.dataset.sectionId;
const hookEndpoint = section.dataset.hookEndpoint;
const buttons = section.querySelectorAll('.btn'); const buttons = section.querySelectorAll('.btn');
// ... // ...
} });
``` ```
### CmsApi (Client-Side) ### CmsApi (Client-Side)
@@ -88,6 +105,25 @@ CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(respon
}); });
``` ```
If you are calling a hook that belongs to the current module, the endpoint must use the real module id:
```js
// Module folder: template/estandar/modulos/buscadorapartados_hjd8s/
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, function(response) {
console.log(response);
});
```
Do not try to build this endpoint with Twig inside `script.js`. Put the final endpoint in a `data-hook-endpoint` attribute in `index-base.tpl` if needed.
### Module Styles (`style.css`)
`style.css` is also a static file.
Do NOT use Twig syntax or builder attributes inside it.
Do NOT write selectors or values that depend on `{{ section_id }}`.
Scope styles with the module root class instead of dynamic Twig ids.
### Cuándo usar Vue 3 ### Cuándo usar Vue 3
Usar Vue 3 CDN cuando la lógica requiera: Usar Vue 3 CDN cuando la lógica requiera:

View File

@@ -2,7 +2,39 @@
## Hooks ## Hooks
Hooks son archivos PHP en `hooks/` que ejecutan lógica server-side. También pueden estar dentro de un módulo en `template/estandar/modulos/<module-id>/hook.php`. Hooks son archivos PHP que ejecutan lógica server-side. Pueden existir en dos sitios:
- Hooks globales en `hooks/hooks.<hook-id>.php`
- Hooks propios de módulo en `template/estandar/modulos/<module-id>/hook.php`
### Tipos de hooks
**1. Hook global**
- Archivo: `hooks/hooks.<hook-id>.php`
- Endpoint: `/hooks/<hook-id>/`
- Úsalo cuando la lógica se reutiliza entre módulos, páginas o formularios
**2. Hook propio de módulo**
- Archivo: `template/estandar/modulos/<module-id>/hook.php`
- Endpoint: `/hooks/<module-id>/`
- Úsalo cuando la lógica pertenece solo a ese módulo
Ejemplos:
- `hooks/hooks.buscar_barcos.php` -> `/hooks/buscar_barcos/`
- `template/estandar/modulos/hero_banner/hook.php` -> `/hooks/hero_banner/`
Regla práctica:
- Si el hook solo sirve a un módulo, créalo dentro del módulo
- Si varias piezas del proyecto lo van a consumir, créalo como hook global
## Reglas obligatorias para hooks
- Un hook debe devolver datos con `return [...]`
- No uses `echo json_encode(...)`
- No uses `exit`
- Para leer parámetros, usa `$_REQUEST[...]` o las variables ya inyectadas por el sistema
- En hooks, usa `CmsApi::get()` o `CocoDB::get()` como primera opción
- No uses `CocoDB::getInstance()` salvo necesidad real muy excepcional
- No escribas SQL manual con `prepare()/bind_param()` salvo que no exista forma razonable de resolverlo con `CmsApi` o `CocoDB`
### Estructura de un Hook ### Estructura de un Hook
@@ -34,6 +66,10 @@ No usar X-Hooks-Token en desarrollo local.
### Cómo Llamar Hooks ### Cómo Llamar Hooks
La referencia siempre se hace con el endpoint `/hooks/<id>/`:
- Para hooks globales, `<id>` es el nombre lógico del hook sin `hooks.` ni `.php`
- Para hooks de módulo, `<id>` es el `module-id` de la carpeta del módulo
**Desde HTML (recomendado para módulos):** **Desde HTML (recomendado para módulos):**
```html ```html
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook> <hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
@@ -53,6 +89,14 @@ CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
}); });
``` ```
**Ejemplo real para hook de módulo:**
```js
// Módulo: template/estandar/modulos/buscadorapartados_hjd8s/
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, (data) => {
console.log(data);
});
```
**Desde otro Hook PHP:** **Desde otro Hook PHP:**
```php ```php
<?php <?php
@@ -71,37 +115,49 @@ API server-side para operaciones de base de datos. Disponible en todos los hooks
### Read — `CmsApi::get()` ### Read — `CmsApi::get()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Todos los registros // Todos los registros
$products = CmsApi::get('productos'); $products = CmsApi::get("productos");
// Con condición WHERE // Con condición WHERE en string
$active = CmsApi::get('productos', ['active' => 1]); $active = CmsApi::get("productos", "active=1");
// Con orden y límite // Con orden y límite
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5); $latest = CmsApi::get("noticias", "", "fecha DESC", 5);
// Con condición string // Con condición string
$activos = CmsApi::get('productos', 'activo=1'); $activos = CmsApi::get("productos", "activo=1");
// Condición compleja como array // Condición compleja
$caros = CmsApi::get('productos', [ $caros = CmsApi::get("productos", "precio > 100");
["column" => "precio", "operator" => ">", "value" => 100]
]);
// Múltiples condiciones (AND) // Múltiples condiciones (AND)
$resultados = CmsApi::get('productos', [ $resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
["column" => "activo", "operator" => "=", "value" => 1],
["column" => "stock", "operator" => ">", "value" => 0]
]);
// Con operadores // Con operadores
$expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]); $expensive = CmsApi::get("productos", "precio >= 100");
$search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]); $search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
$inList = CmsApi::get('productos', ['categoria_num' => ['IN' => [1, 2, 3]]]); $inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
#### Opciones de `get()`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
| `translates` | string | current lang | Código de idioma |
| `groupBy` | string | null | GROUP BY clause |
| `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo campos específicos |
| `debug` | bool | false | Mostrar SQL query |
| `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
// Con opciones // Con opciones
$datos = CmsApi::get('productos', '', '', '', [ $datos = CmsApi::get("productos", "", "", "", [
'translates' => true, 'translates' => true,
'uploads' => true, 'uploads' => true,
'relations' => true, 'relations' => true,
@@ -111,6 +167,8 @@ $datos = CmsApi::get('productos', '', '', '', [
### Insert — `CmsApi::insert()` ### Insert — `CmsApi::insert()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Un registro // Un registro
CmsApi::insert('contacto', [ CmsApi::insert('contacto', [
@@ -131,8 +189,19 @@ CmsApi::insert('productos',
); );
``` ```
#### Opciones de insert
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### Update — `CmsApi::update()` ### Update — `CmsApi::update()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Con condición string // Con condición string
CmsApi::update('productos', ["precio" => 150], "num=1"); CmsApi::update('productos', ["precio" => 150], "num=1");
@@ -147,8 +216,19 @@ CmsApi::update('productos',
CmsApi::update('productos', ["activo" => 0], "precio < 50"); CmsApi::update('productos', ["activo" => 0], "precio < 50");
``` ```
#### Opciones de update
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### Delete — `CmsApi::delete()` ### Delete — `CmsApi::delete()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
CmsApi::delete('productos', "num=5"); CmsApi::delete('productos', "num=5");
@@ -187,134 +267,24 @@ CmsApi.get('tableName', { where: conditions }, function(records) {
Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control. Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control.
Para búsquedas y lecturas habituales, prioriza:
1. `CmsApi::get()`
2. `CocoDB::get()`
3. SQL manual solo si de verdad no hay alternativa razonable
### `CocoDB::get($table, $where, $order, $limit, $options)` ### `CocoDB::get($table, $where, $order, $limit, $options)`
## Funcionalidad exactamente igual a CmsApi::get ( ver referencia CmsApi::get )
```php
// Básico
$records = CocoDB::get('productos', ['activo' => 1], 'orden ASC', 10);
// Where con operadores avanzados
$records = CocoDB::get('productos', [
['column' => 'precio', 'value' => 100, 'operator' => '>='],
['column' => 'categoria_num', 'value' => [1, 2, 3], 'operator' => 'IN'],
]);
// Condiciones OR
$records = CocoDB::get('productos', [
['column' => 'nombre', 'value' => '%keyword%', 'operator' => 'LIKE'],
['column' => 'descripcion', 'value' => '%keyword%', 'operator' => 'LIKE', 'or' => true],
]);
// NOT
$records = CocoDB::get('productos', [
['column' => 'estado', 'value' => 'borrador', 'operator' => '=', 'not' => true],
]);
// IS NULL
$records = CocoDB::get('productos', [
['column' => 'fecha_baja', 'value' => '', 'operator' => 'IS NULL'],
]);
// Limit con offset
$records = CocoDB::get('productos', [], 'num DESC', ['limit' => 10, 'offset' => 20]);
```
#### Opciones de `get()`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
| `translates` | string | current lang | Código de idioma |
| `groupBy` | string | null | GROUP BY clause |
| `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo campos específicos |
| `debug` | bool | false | Mostrar SQL query |
| `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
### `CocoDB::insertRecords($table, $records, $functions, $options)` ### `CocoDB::insertRecords($table, $records, $functions, $options)`
## Funcionalidad exactamente igual a CmsApi::insert ( ver referencia CmsApi::insert )
```php
// Un registro
$count = CocoDB::insertRecords('contacto', [
'nombre' => 'John',
'email' => 'john@example.com',
]);
// Usar mysql_insert_id() para obtener el nuevo num
// Múltiples
$count = CocoDB::insertRecords('productos', [
['nombre' => 'Product A', 'precio' => 10],
['nombre' => 'Product B', 'precio' => 20],
]);
```
#### Opciones de insert/update
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### `CocoDB::updateRecords($table, $records, $where, $functions, $options)` ### `CocoDB::updateRecords($table, $records, $where, $functions, $options)`
## Funcionalidad exactamente igual a CmsApi::update ( ver referencia CmsApi::update )
```php
CocoDB::updateRecords('productos',
['precio' => 29.99, 'activo' => 1],
['num' => 42]
);
// Con operador en where
CocoDB::updateRecords('productos',
['activo' => 0],
[['column' => 'stock', 'value' => 0, 'operator' => '<=']]
);
```
### `CocoDB::deleteRecords($table, $where, $options)` ### `CocoDB::deleteRecords($table, $where, $options)`
## Funcionalidad exactamente igual a CmsApi::delete ( ver referencia CmsApi::delete )
```php
CocoDB::deleteRecords('productos', ['num' => 42]);
CocoDB::deleteRecords('logs', [
['column' => 'fecha', 'value' => '2024-01-01', 'operator' => '<']
]);
```
### Parámetro `$functions`
Permite aplicar funciones MySQL a valores durante insert/update:
```php
CocoDB::insertRecords('logs', [
'mensaje' => 'Login exitoso',
'fecha' => '',
], [
'fecha' => 'NOW()',
]);
```
### Where Clause — Formatos
**Simple (key-value):**
```php
['campo' => 'valor'] // campo = 'valor'
```
**Avanzado (array de condiciones):**
```php
[
'column' => 'field_name',
'value' => 'match_value',
'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL
'or' => false, // OR en vez de AND
'not' => false, // Negar la condición
'raw_key' => false, // Saltar check de existencia de columna
]
```
--- ---
@@ -379,12 +349,15 @@ return [
<p>Total: ${{ precio.total }}</p> <p>Total: ${{ precio.total }}</p>
``` ```
En este ejemplo, el endpoint usa `calcular_precio` porque el archivo vive en:
`template/estandar/modulos/calcular_precio/hook.php`
### Hook con Operaciones de BD ### Hook con Operaciones de BD
```php ```php
<?php <?php
// hook.php del módulo "procesar_compra" // hook.php del módulo "procesar_compra"
$producto = CmsApi::get('productos', "num=$producto_id"); $producto = CmsApi::get("productos", "num=" . $producto_id);
if (empty($producto)) { if (empty($producto)) {
return ["success" => false, "message" => "Producto no encontrado"]; return ["success" => false, "message" => "Producto no encontrado"];
@@ -402,7 +375,7 @@ CmsApi::insert('ventas', [[
]], [], ['return_last_id' => true]); ]], [], ['return_last_id' => true]);
// Actualizar stock // Actualizar stock
$stock = CmsApi::get('stocks', "producto_num=$producto_id"); $stock = CmsApi::get("stocks", "producto_num=" . $producto_id);
if (!empty($stock)) { if (!empty($stock)) {
CmsApi::update('stocks', CmsApi::update('stocks',
["cantidad" => $stock[0]['cantidad'] - $cantidad], ["cantidad" => $stock[0]['cantidad'] - $cantidad],

View File

@@ -73,6 +73,20 @@ PHP files that execute server-side logic. Triggered by:
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)` - JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
- Form action: via `c-form` attribute - Form action: via `c-form` attribute
There are two valid hook locations:
- Global hooks in `hooks/hooks.<hook-id>.php` for reusable/shared server-side logic
- Module-specific hooks in `template/estandar/modulos/<module-id>/hook.php` for logic owned by a single module
How to reference them:
- Global hook `hooks/hooks.calcular_precio.php` -> endpoint `/hooks/calcular_precio/`
- Module hook `template/estandar/modulos/hero_banner/hook.php` -> endpoint `/hooks/hero_banner/`
- Module hook `template/estandar/modulos/buscadorapartados_hjd8s/hook.php` -> endpoint `/hooks/buscadorapartados_hjd8s/`
Rule of thumb:
- If the logic is only used by one module, prefer that module's `hook.php`
- If the logic will be reused by several modules/pages, create a global hook in `hooks/`
- Return arrays from hooks; do not use `echo json_encode(...)` or `exit`
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage. See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
## Database Access ## Database Access
@@ -103,17 +117,18 @@ Do NOT modify web-base files — they are shared across all projects.
## Critical Rules ## Critical Rules
1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action. 1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action.
2. **NEVER use `mkdir` to create directories.** Instead, use the `Write` tool to create the first file inside the directory — this creates parent directories automatically. For example, to create a new module, directly write the `index-base.tpl` file. 2. **NEVER use `mkdir` to create directories.** Instead, use `acai-write` to create the first file inside the directory — this creates parent directories automatically. For example, to create a new module, directly write the `index-base.tpl` file.
3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated 3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
3. **Edit `index-base.tpl` using `acai_write` or `acai_line_replace`** — the server compiles automatically when the file is saved. No need to call `compile_module` manually. 4. Editing or creating any `index-base.tpl` through `acai-write` or `acai-line-replace` triggers automatic compilation. `compile_module` is only for manual recovery when you need to force a recompile without changing the file.
4. Use Twig **filters** (with `|`), never Twig functions 5. `script.js` and `style.css` are static files — do NOT use Twig syntax inside them. Pass dynamic values from `index-base.tpl` via `data-*` attributes.
5. Table names without `cms_` prefix everywhere 6. Use Twig **filters** (with `|`), never Twig functions
6. Primary key is `num`, never `id` 7. Table names without `cms_` prefix everywhere
7. Upload fields are arrays — access with `[0].urlPath` 8. Primary key is `num`, never `id`
8. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed 9. Upload fields are arrays — access with `[0].urlPath`
9. Twig concatenation uses `~` operator: `'value=' ~ variable` 10. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
10. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked 11. Twig concatenation uses `~` operator: `'value=' ~ variable`
11. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard 12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked
13. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard
## MCP Tools ## MCP Tools
@@ -122,8 +137,8 @@ This project has MCP tools for managing modules, records, media, and more. **Bef
See [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) for the complete list of available tools and step-by-step workflows. See [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) for the complete list of available tools and step-by-step workflows.
Key workflows: Key workflows:
- **Create module**: Read [docs/module-creation-guide.md](docs/module-creation-guide.md) first → Write `index-base.tpl` via `acai_write``add_module_to_record` (returns sectionId) → `set_module_config_vars` (returns uploadFields) → images via uploadFields - **Create module**: Read [docs/module-creation-guide.md](docs/module-creation-guide.md) first → create files with `acai-write` / refine with `acai-line-replace` → automatic compile on `index-base.tpl``add_module_to_record` (returns sectionId) → `set_module_config_vars` (returns uploadFields) → images via uploadFields. Use `compile_module` only if you need a manual recompile without editing the file.
- **Edit module**: read vars → edit `index-base.tpl` with `acai_write` or `acai_line_replace` (server compiles automatically) - **Edit module**: `acai-view``acai-line-replace` (or `acai-write` for full rewrites) → automatic compile on `index-base.tpl`
- **Add images**: use `uploadFields` from `set_module_config_vars` response → `upload_record_image` - **Add images**: use `uploadFields` from `set_module_config_vars` response → `upload_record_image`
- **Generate images**: `generate_image``upload_record_image` with returned URL - **Generate images**: `generate_image``upload_record_image` with returned URL

View File

@@ -4,11 +4,15 @@
| Tool | Categoría | Acción | | Tool | Categoría | Acción |
|------|-----------|--------| |------|-----------|--------|
| `create_module` | Módulos | (Legacy) Alternativa para crear módulo — preferir acai_write | | `compile_module` | Módulos | Recompilación manual de rescate cuando necesitas forzar la compilación sin editar el archivo |
| `compile_module` | Módulos | Compila módulo tras editar index-base.tpl |
| `check_module` | Módulos | Preview de cómo renderiza un módulo | | `check_module` | Módulos | Preview de cómo renderiza un módulo |
| `check_module_usage` | Módulos | Qué páginas usan un módulo | | `check_module_usage` | Módulos | Qué páginas usan un módulo |
| `set_module_example_data` | Módulos | Datos de ejemplo para editor visual | | `acai-view` | Archivos | Lee un archivo del proyecto por tramos |
| `acai-glob` | Archivos | Busca rutas de archivos por patrón |
| `acai-grep` | Archivos | Busca texto en archivos del proyecto |
| `acai-line-replace` | Archivos | Reemplaza un bloque concreto en un archivo existente |
| `acai-write` | Archivos | Crea o reescribe un archivo completo. Antes de usarlo, lee la doc correspondiente según el tipo de archivo (`module-creation-guide`, `builder-fields`, `css-js-conventions`, `hooks-and-api`) |
| `acai-delete` | Archivos | Borra un archivo del proyecto |
| `list_page_modules` | Registros | Lista módulos de una página | | `list_page_modules` | Registros | Lista módulos de una página |
| `add_module_to_record` | Registros | Añade módulo a una página | | `add_module_to_record` | Registros | Añade módulo a una página |
| `remove_module_from_record` | Registros | Elimina módulo de una página | | `remove_module_from_record` | Registros | Elimina módulo de una página |
@@ -30,29 +34,43 @@
| `refresh_acai_token` | Auth | Renovar token JWT expirado | | `refresh_acai_token` | Auth | Renovar token JWT expirado |
| `navigate_browser` | Navegación | Navegar el browser del frontend a una URL | | `navigate_browser` | Navegación | Navegar el browser del frontend a una URL |
| `save_project_styles` | Proyecto | Guardar resumen de estilos en docs/project-styles.md | | `save_project_styles` | Proyecto | Guardar resumen de estilos en docs/project-styles.md |
| `orchestrate_task` | Orquestador | Guía paso a paso para tareas complejas |
| `rollback_git` | Git | Recuperar cambios de git remoto | | `rollback_git` | Git | Recuperar cambios de git remoto |
## Flujos de trabajo ## Flujos de trabajo
### Crear un módulo nuevo desde cero ### Crear un módulo nuevo desde cero
1. `acai_write`Escribe `index-base.tpl` en `template/estandar/modulos/NOMBRE/`. El server crea la carpeta si no existe, compila y genera todos los archivos derivados (index-twig.tpl, index.tpl, builder.json, screenshots) 1. `acai-write`Crea `index-base.tpl`, `style.css`, `script.js` y cualquier hook necesario con rutas relativas al proyecto
2. `add_module_to_record` — Añade el módulo a una página (tabla padre, ej: `apartados`) 2. `acai-write` o `acai-line-replace` compilan automáticamente al tocar `index-base.tpl`
3. `set_module_config_vars` — Rellena las variables con contenido (textos, colores, opciones). **OBLIGATORIO** — sin esto el módulo no muestra nada. Devuelve: 3. `add_module_to_record` — Añade el módulo a una página (tabla padre, ej: `apartados`)
4. `set_module_config_vars` — Rellena las variables con contenido (textos, colores, opciones). **OBLIGATORIO** — sin esto el módulo no muestra nada. Devuelve:
- `configVars`: mapa de variables → recordNums - `configVars`: mapa de variables → recordNums
- `uploadFields`: mapa de variables upload → `{ fieldName, recordNum }`**usa estos directamente** para subir imágenes sin necesidad de leer builder.json - `uploadFields`: mapa de variables upload → `{ fieldName, recordNum }`**usa estos directamente** para subir imágenes sin necesidad de leer builder.json
- Para vars multi con uploads: `uploadFields["varName.subVarName"]` es un array con `[{ index, fieldName, recordNum }]` - Para vars multi con uploads: `uploadFields["varName.subVarName"]` es un array con `[{ index, fieldName, recordNum }]`
4. Para imágenes: `generate_image` o `upload_record_image` usando el `recordNum` y `fieldName` del `uploadFields` devuelto en el paso 3 5. Para imágenes: `generate_image` o `upload_record_image` usando el `recordNum` y `fieldName` del `uploadFields` devuelto en el paso 4
5. Verificar con `check_module` o recargando la página 6. Verificar con `check_module` o recargando la página
> **Nota:** `create_module` es una alternativa legacy que hace lo mismo pero con menos control sobre el contenido del template.
### Editar un módulo existente ### Editar un módulo existente
1. `get_module_config_vars` — Leer el estado actual del módulo (variables, recordNums) 1. `get_module_config_vars` — Leer el estado actual del módulo (variables, recordNums)
2. Editar `index-base.tpl` con `acai_write` o `acai_line_replace` — el server compila automáticamente al guardar 2. `acai-view` — Leer solo el tramo de `index-base.tpl` que se va a modificar
3. Si cambias variables: `set_module_config_vars` para actualizar valores 3. `acai-line-replace` — Editar el bloque concreto. Usa `acai-write` solo si el archivo es nuevo o el cambio es masivo
4. La compilación es automática tras cada edición de `index-base.tpl`
5. Si cambias variables: `set_module_config_vars` para actualizar valores
### Editar archivos del proyecto con bajo consumo de tokens
1. `acai-view` — Leer el archivo o un rango de líneas
2. `acai-glob` — Encontrar archivos relevantes por ruta cuando no conoces el path exacto
3. `acai-grep` — Buscar texto o atributos concretos dentro de archivos del proyecto
4. `acai-line-replace` — Reemplazar el bloque exacto en archivos existentes
5. `acai-write` — Crear archivos nuevos o reescribirlos por completo si es necesario
6. `acai-delete` — Borrar archivos solo cuando sea explícitamente necesario
Reglas:
- Usa siempre rutas relativas al proyecto
- No edites `index.tpl`, `index-twig.tpl` ni `builder.json` — son auto-generados
- Tras editar cualquier `index-base.tpl` con las file tools, la compilación se ejecuta automáticamente
### Añadir/modificar imágenes de un módulo ### Añadir/modificar imágenes de un módulo
@@ -68,13 +86,13 @@
- `tableName`: `"builder_custom"` (siempre sin cms_) - `tableName`: `"builder_custom"` (siempre sin cms_)
- `recordId`: el recordNum del paso 1 - `recordId`: el recordNum del paso 1
- `fieldName`: el campo de relations del builder.json (ej: `image1`) - `fieldName`: el campo de relations del builder.json (ej: `image1`)
- `imageUrl`: URL accesible desde Docker (ej: `http://localhost/cms/uploads/...`) - `imageUrl`: usa la URL recomendada por la tool que generó/subió la imagen. En Forge, si `generate_image` devuelve `uploadUrl` o `fullUrl`, priorízala frente a `dockerUrl`
### Generar imagen con IA ### Generar imagen con IA
1. `generate_image` con prompt descriptivo + style (photographic, digital-art, minimalist...) 1. `generate_image` con prompt descriptivo + style (photographic, digital-art, minimalist...)
2. La imagen se guarda en `cms/uploads/generated/` y devuelve `dockerUrl` 2. La imagen se guarda en `cms/uploads/generated/` y devuelve una URL local de preview (`dockerUrl`) y, cuando aplica, una URL recomendada para subida (`uploadUrl` / `fullUrl`)
3. Usar esa `dockerUrl` con `upload_record_image` para asignarla a un módulo 3. Para `upload_record_image`, usa la URL recomendada por la tool. En Forge, prioriza `uploadUrl` o `fullUrl` si están presentes
### Gestionar registros de una tabla ### Gestionar registros de una tabla
@@ -85,10 +103,9 @@
### Explorar el sitio ### Explorar el sitio
1. `orchestrate_task` con workflow `explore_site` — Guía para entender la estructura 1. `list_page_modules` — Ver qué módulos tiene cada página
2. `list_page_modules` — Ver qué módulos tiene cada página 2. `get_module_config_vars` — Ver los datos de cada módulo
3. `get_module_config_vars` — Ver los datos de cada módulo 3. `check_module` — Preview de cómo renderiza
4. `check_module` — Preview de cómo renderiza
## Reglas importantes para todas las tools ## Reglas importantes para todas las tools

View File

@@ -37,12 +37,35 @@ Each module lives in `template/estandar/modulos/<moduleId>/` with:
## Creating a Module — Full Workflow ## Creating a Module — Full Workflow
If the module needs JavaScript, you MUST read `docs/css-js-conventions.md` before writing `index-base.tpl` or `script.js`.
If the module needs server-side logic, dynamic data processing, form handling, or reusable backend behavior, you MUST read `docs/hooks-and-api.md` before creating `hook.php` or any global hook.
If the module will call hooks from Twig, also review `docs/twig-filters.md` for the `hook` filter syntax.
If `index-base.tpl` contains builder fields (`data-field-type`), you MUST review `docs/builder-fields.md` before writing the template.
Hard rules for module files:
- `index-base.tpl` is for HTML/Twig only
- `script.js` is for module JavaScript
- `style.css` is for module CSS
- `hook.php` is for server-side logic
- Do NOT embed `<script>` tags in `index-base.tpl`
- Do NOT put PHP logic in `index-base.tpl`
- `script.js` and `style.css` are static files: do NOT use Twig syntax inside them
- Do NOT use `{{ ... }}`, `{% ... %}`, `c-if`, `c-for`, or builder attributes inside `script.js` or `style.css`
- If JavaScript needs dynamic values such as `section_id` or a hook endpoint, expose them from `index-base.tpl` via `data-*` attributes
- Every editable builder field with `data-field-type` MUST also define `data-field-label`
- Avoid Tailwind arbitrary-value syntax such as `text-[...]`, `font-[...]`, `leading-[...]`, `bg-[...]` inside `index-base.tpl`; move those styles to `style.css`
1. **Read style reference** (steps above) 1. **Read style reference** (steps above)
2. **`acai_write`** — Write `index-base.tpl` to `template/estandar/modulos/MODULE_ID/index-base.tpl`. The server automatically creates the directory, compiles and generates all derived files. `create_module` is a legacy alternative. 2. **`acai-write`** — Create the module files directly (`index-base.tpl`, `style.css`, `script.js`, optional `hook.php`) using project-relative paths and complete file contents.
3. **`add_module_to_record`** — Adds the module to a page. Response includes `sectionId` — use it directly in the next step. - If the server-side logic belongs only to that module, create `template/estandar/modulos/<module-id>/hook.php`
4. **`set_module_config_vars`** — Fill variables with content. Response includes `uploadFields` with `{ fieldName, recordNum }` for each upload variable. - If the logic should be reused across modules/pages, create a global hook in `hooks/hooks.<hook-id>.php`
5. **Upload images** — Use `generate_image` then `upload_record_image` with the `recordNum` and `fieldName` from step 4's `uploadFields`. No need to read builder.json or call get_module_config_vars. - Inside the module, reference its own hook with `/hooks/<module-id>/`
6. **`navigate_browser`** — Navigate to the page so the user can see the result. - Example: module folder `template/estandar/modulos/buscadorapartados_hjd8s/` -> hook endpoint `/hooks/buscadorapartados_hjd8s/`
3. **Automatic compile** — Writing `index-base.tpl` automatically creates the generated template placeholders and triggers compilation. `compile_module` is only a manual recovery tool if you need to force a recompile without changing the file.
4. **`add_module_to_record`** — Adds the module to a page. Response includes `sectionId` — use it directly in the next step.
5. **`set_module_config_vars`** — Fill variables with content. Response includes `uploadFields` with `{ fieldName, recordNum }` for each upload variable.
6. **Upload images** — Use `generate_image` then `upload_record_image` with the `recordNum` and `fieldName` from step 5's `uploadFields`. No need to read builder.json or call get_module_config_vars.
7. **`navigate_browser`** — Navigate to the page so the user can see the result.
## HTML Field Types ## HTML Field Types
@@ -72,6 +95,6 @@ Modules with `MJMLModule: true` in their schema are email modules:
- Use `section_id` variable for unique anchors/scoping - Use `section_id` variable for unique anchors/scoping
- Use `interno` variable to detect CMS editor vs public view - Use `interno` variable to detect CMS editor vs public view
- Include other modules with: `<module_id :param1="value1"></module_id>` - Include other modules with: `<module_id :param1="value1"></module_id>`
- After editing `index-base.tpl` with `acai_write` or `acai_line_replace`, the server compiles automatically — no need to call `compile_module` - Editing `index-base.tpl` with `acai-write` or `acai-line-replace` compiles automatically
- Twig uses filters (with `|`), never functions - Twig uses filters (with `|`), never functions
- Twig concatenation uses `~`: `'value=' ~ variable` - Twig concatenation uses `~`: `'value=' ~ variable`

View File

@@ -70,7 +70,7 @@ The controlador defines whether the page is Builder or Standard. Changing it bre
1. List current modules: `list_page_modules` 1. List current modules: `list_page_modules`
2. Get module vars: `get_module_config_vars` 2. Get module vars: `get_module_config_vars`
3. Update vars: `set_module_config_vars` 3. Update vars: `set_module_config_vars`
4. Or edit the module template: edit `index-base.tpl` `compile_module` 4. Or edit the module template: edit `index-base.tpl` with `acai-line-replace` or `acai-write` → compilation runs automatically
## Working with Standard Pages ## Working with Standard Pages

View File

@@ -16,7 +16,7 @@ from ..context.engine import ContextEngine
from ..mcp.manager import MCPManager from ..mcp.manager import MCPManager
from ..memory.store import MemoryStore from ..memory.store import MemoryStore
from ..models.agent import AgentRole from ..models.agent import AgentRole
from ..models.session import SessionState, SessionStatus, TaskState, TaskStatus from ..models.session import SessionState, SessionStatus, TaskState, TaskStatus, TaskStep
from ..streaming.sse import SSEEmitter, EventType from ..streaming.sse import SSEEmitter, EventType
from .agents.coder import CoderAgent, create_coder_profile from .agents.coder import CoderAgent, create_coder_profile
from .agents.collector import CollectorAgent, create_collector_profile from .agents.collector import CollectorAgent, create_collector_profile
@@ -182,6 +182,20 @@ class OrchestratorEngine:
"total_cost_usd": round(cost_usd, 6), "total_cost_usd": round(cost_usd, 6),
} }
# Enforce max 2 steps: 1 coder + 1 optional reviewer
# The coder is capable enough to do everything in 1 step
if len(plan_result) > 2:
logger.warning("Plan had %d steps, trimming to 2", len(plan_result))
# Keep first coder step + merge descriptions of remaining into it
merged_desc = "; ".join(s.description for s in plan_result)
plan_result[0].description = merged_desc
# Add reviewer if any step was reviewer
has_reviewer = any(s.agent_role == "reviewer" for s in plan_result)
if has_reviewer:
plan_result = [plan_result[0], TaskStep(description="Revisar el resultado", agent_role="reviewer")]
else:
plan_result = [plan_result[0]]
task.plan = plan_result task.plan = plan_result
task.status = TaskStatus.EXECUTING task.status = TaskStatus.EXECUTING
except Exception as e: except Exception as e: