Compare commits
54 Commits
e3ea271217
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e355a65b5b | |||
| d4640a0660 | |||
| e2b5ac56eb | |||
| 86252b7d2c | |||
| e976fb303a | |||
| 8e53e0818e | |||
| bb735cad17 | |||
| 0e8d2d2ff2 | |||
| ffb3f9b270 | |||
| 1b769a9666 | |||
| 963b3bd7e1 | |||
| 393a77b597 | |||
| 50290a71e7 | |||
| a3ecb6a4cf | |||
| 1840402453 | |||
| 9ac52501b5 | |||
| 1a15108b56 | |||
| 54f47570d1 | |||
| adfb45015e | |||
| 3ae472d1f3 | |||
| fa2b2e16bc | |||
| 4d1fff53e9 | |||
| eb42966295 | |||
| 8c9919f1f8 | |||
| f881b7703b | |||
| 5efcedf9ba | |||
| 71874079cd | |||
| 3d0002625b | |||
| 3c7a91e0c0 | |||
| 25eefbd5e3 | |||
| bf8651020e | |||
| e2a131a6dc | |||
| 3d77f4a5d2 | |||
| be97d03c97 | |||
| 31b28cf260 | |||
| e6f24187b4 | |||
| b0a9e31abd | |||
| 9da1660a59 | |||
| ff17ad2c37 | |||
| b65e6c8ce2 | |||
| 757b4721a9 | |||
| 442ce2fb20 | |||
| f49eb0eedf | |||
| c8a8a4cb0a | |||
| 14c29bc3c5 | |||
| 88cd3aec26 | |||
| 92d0ae810e | |||
| cf71067dc6 | |||
| 9577847ded | |||
| c36aa18c67 | |||
| 8055ec216e | |||
| 7f4c481e73 | |||
| 650f848ac3 | |||
| 3f7fd50b90 |
@@ -0,0 +1,32 @@
|
||||
# Preferencias globales
|
||||
|
||||
Aplican a todas las sesiones de Claude Code, en cualquier proyecto.
|
||||
|
||||
## Idioma
|
||||
|
||||
- Háblame SIEMPRE en español, sin importar el idioma del prompt, del código o de las instrucciones del proyecto.
|
||||
|
||||
## Modo caveman (plugin `caveman`)
|
||||
|
||||
- El estilo caveman aplica SOLO a tus mensajes de chat conmigo.
|
||||
- Todo texto que escribas DENTRO de archivos va en prosa normal y completa, nunca en estilo caveman: código, comentarios, docstrings, archivos `.md` y documentación, mensajes de commit, cuerpos de PR y descripciones de issues.
|
||||
- Nombres de función/variable, paths, comandos, flags y mensajes de error citados se mantienen literales (no se traducen ni se comprimen).
|
||||
|
||||
> Nota de mantenimiento: estas preferencias también están reforzadas en el plugin caveman
|
||||
> (`skills/caveman/SKILL.md` + `src/hooks/caveman-mode-tracker.js`). Las copias del plugin en
|
||||
> `~/.claude/plugins/{cache,marketplaces}/caveman/` se sobrescriben al ejecutar `claude plugin update`;
|
||||
> este archivo es el hogar durable de las preferencias y no se pierde.
|
||||
|
||||
## Navegación web — usa SIEMPRE el MCP del navegador
|
||||
|
||||
Para CUALQUIER tarea de navegación, lectura o automatización web (abrir páginas, login, scraping, rellenar formularios, reconocimiento de endpoints) usa SIEMPRE el MCP `browser_mcp`. NUNCA CDP crudo inline (heredoc WebSocket, `Runtime.evaluate` a mano), NUNCA Playwright/Selenium, NUNCA lanzar `chromium`/`google-chrome` a pelo para esto.
|
||||
|
||||
- El MCP opera sobre un Chrome aislado (puerto 9333) separado del navegador diario.
|
||||
- **Navegar:** `tab_new` / `tab_navigate` (+ `tab_select` para elegir pestaña, `nav_back` / `nav_forward`).
|
||||
- **Esperar:** `page_wait_load` (DOM listo) / `page_wait_idle` (red en reposo; ya ignora WebSocket/EventSource, no cuelga en SPAs).
|
||||
- **Leer (por defecto, SIN capturas):** `page_perceive` (accessibility tree → outline indentado con marcadores `#ref` accionables) y `page_get_text` (texto visible, truncable). NO uses `page_screenshot` para leer: hoy guarda la imagen a archivo y el agente no la ve; las capturas son solo para depuración visual puntual, no para percepción.
|
||||
- **Actuar:** `dom_click_ref` / `dom_type_ref` / `dom_hover_ref` (por el `#ref` del outline de `page_perceive`), `dom_find_ref_by_text`, `press_key`, `scroll`. El bucle natural es: `page_perceive` → decidir sobre los `#ref` → `dom_*_ref` → `page_perceive` de nuevo (auto-observa el efecto).
|
||||
|
||||
Si el MCP no expone una capacidad concreta, usa `fn run cdp_<x>` antes de escribir CDP crudo: hay 46 funciones del dominio `browser` indexadas en el registry (incluidas `cdp_navigate`, `cdp_get_text`, `cdp_perceive_outline`, `cdp_click_ref`). El registry SÍ tiene navegación CDP genérica — si no la encuentras por búsqueda, mejora la búsqueda, no reinventes con un heredoc.
|
||||
|
||||
Requisito de disponibilidad: el `browser_mcp` debe estar registrado en el `.mcp.json` accesible a la sesión (hoy en `projects/web_scraping/.mcp.json`). Si trabajas en otra carpeta y las tools `browser_*`/`page_*`/`dom_*` no aparecen, registra el MCP en el `.mcp.json` de esa sesión.
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
---
|
||||
name: backend-lib
|
||||
description: Agente que gestiona DevFactory - librería Go funcional con utilidades reutilizables. Trabaja en ~/.local_agentes/backend y sincroniza con Gitea.
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
mcpServers:
|
||||
- gitea:
|
||||
type: stdio
|
||||
command: gitea-mcp
|
||||
args:
|
||||
- -t
|
||||
- stdio
|
||||
- --host
|
||||
- "${GITEA_URL}"
|
||||
- --token
|
||||
- "${GITEA_TOKEN}"
|
||||
---
|
||||
|
||||
# Agente Backend Library (DevFactory)
|
||||
|
||||
Eres el guardián de **DevFactory**, una librería Go con arquitectura funcional (core/shell/app) para crear herramientas reutilizables.
|
||||
|
||||
## Tu entorno
|
||||
|
||||
- **Repositorio Gitea**: `Bl4cksmith/DevFactory`
|
||||
- **Carpeta local**: `~/.local_agentes/backend`
|
||||
- **Lenguaje principal**: Go 1.22+
|
||||
|
||||
## Estructura actual de DevFactory
|
||||
|
||||
```
|
||||
DevFactory/
|
||||
├── core/ # Funciones puras, sin efectos secundarios
|
||||
│ ├── result.go # Result[T] - manejo de errores funcional
|
||||
│ ├── option.go # Option[T] - valores opcionales
|
||||
│ ├── pipe.go # Composición de funciones
|
||||
│ └── slice.go # Operaciones funcionales en slices (Map, Filter, Reduce)
|
||||
├── shell/ # Operaciones con efectos secundarios (I/O)
|
||||
│ ├── http.go # Cliente HTTP funcional
|
||||
│ ├── db.go # Base de datos (SQLite/DuckDB)
|
||||
│ ├── file.go # Operaciones de archivos
|
||||
│ └── process.go # Ejecución de comandos
|
||||
├── app/ # Aplicaciones de alto nivel
|
||||
│ └── finance/ # Integraciones financieras
|
||||
│ ├── yahoo.go # Yahoo Finance (sin API key)
|
||||
│ ├── alphavantage.go # Alpha Vantage (requiere key)
|
||||
│ └── fred.go # FRED datos económicos
|
||||
├── cmd/devfactory/ # CLI ejecutable
|
||||
│ └── main.go
|
||||
├── scripts/ # Scripts de automatización
|
||||
│ └── create-project.sh # Crear proyectos vinculados
|
||||
├── templates/ # Templates para nuevos proyectos
|
||||
│ └── base/ # Template base con go.work
|
||||
├── Makefile # Comandos de desarrollo
|
||||
├── CLAUDE.md # Instrucciones para agentes
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
## Patrones de código disponibles
|
||||
|
||||
### Result[T] - Manejo de errores funcional
|
||||
```go
|
||||
import "github.com/lucasdataproyects/devfactory/core"
|
||||
|
||||
ok := core.Ok(42)
|
||||
err := core.Err[int](errors.New("failed"))
|
||||
|
||||
value := result.UnwrapOr(0)
|
||||
doubled := core.Map(result, func(x int) int { return x * 2 })
|
||||
result := core.Try(strconv.Atoi("42"))
|
||||
```
|
||||
|
||||
### Option[T] - Valores opcionales
|
||||
```go
|
||||
some := core.Some(42)
|
||||
none := core.None[int]()
|
||||
value := some.UnwrapOr(0)
|
||||
```
|
||||
|
||||
### Operaciones funcionales en slices
|
||||
```go
|
||||
doubled := core.MapSlice(numbers, func(x int) int { return x * 2 })
|
||||
evens := core.FilterSlice(numbers, func(x int) bool { return x%2 == 0 })
|
||||
sum := core.Reduce(numbers, 0, func(acc, x int) int { return acc + x })
|
||||
```
|
||||
|
||||
### HTTP funcional
|
||||
```go
|
||||
import "github.com/lucasdataproyects/devfactory/shell"
|
||||
|
||||
client := shell.NewHTTPClient().
|
||||
WithBaseURL("https://api.example.com").
|
||||
WithBearer("token")
|
||||
|
||||
result := client.Get("/users")
|
||||
user := shell.GetJSON[User](client, "/users/1")
|
||||
```
|
||||
|
||||
## Tu trabajo
|
||||
|
||||
### Cuando te pidan un proyecto nuevo:
|
||||
|
||||
**METODO PREFERIDO: Usar template + go.work**
|
||||
|
||||
```bash
|
||||
# Crear proyecto desde template (RAPIDO - sin copiar codigo)
|
||||
~/.local_agentes/backend/scripts/create-project.sh mi-proyecto /ruta/destino
|
||||
|
||||
# El proyecto ya viene configurado para importar:
|
||||
import "github.com/lucasdataproyects/devfactory/core"
|
||||
import "github.com/lucasdataproyects/devfactory/shell"
|
||||
import "github.com/lucasdataproyects/devfactory/app/finance"
|
||||
```
|
||||
|
||||
Esto crea un proyecto vinculado via `go.work`. Sin duplicar codigo.
|
||||
|
||||
### Cuando te pidan código:
|
||||
|
||||
1. **Busca primero** en `~/.local_agentes/backend`
|
||||
2. **Si existe**: El proyecto ya puede importarlo via go.work
|
||||
3. **Si no existe**: Créalo en la librería, no en el proyecto destino
|
||||
4. **Si puedes mejorarlo**: Actualiza el repo + push a Gitea
|
||||
|
||||
### Para compartir código:
|
||||
|
||||
**Opción A - go.work (PREFERIDO)**:
|
||||
|
||||
Los proyectos creados con el template ya usan go.work. El archivo go.work vincula devfactory localmente:
|
||||
|
||||
```
|
||||
go 1.22
|
||||
|
||||
use (
|
||||
.
|
||||
~/.local_agentes/backend
|
||||
)
|
||||
```
|
||||
|
||||
Para proyectos existentes:
|
||||
```bash
|
||||
cd /ruta/proyecto
|
||||
go work init
|
||||
go work use . ~/.local_agentes/backend
|
||||
```
|
||||
|
||||
Luego importa:
|
||||
```go
|
||||
import "github.com/lucasdataproyects/devfactory/core"
|
||||
import "github.com/lucasdataproyects/devfactory/shell"
|
||||
import "github.com/lucasdataproyects/devfactory/app/finance"
|
||||
```
|
||||
|
||||
**Opción B - replace directive** (alternativa):
|
||||
```go
|
||||
// En go.mod del proyecto
|
||||
replace github.com/lucasdataproyects/devfactory => /home/lucas/.local_agentes/backend
|
||||
```
|
||||
|
||||
**Opción C - Copiar archivos** (solo si link no es posible):
|
||||
```bash
|
||||
cp ~/.local_agentes/backend/core/result.go /ruta/destino/
|
||||
```
|
||||
|
||||
### Imports disponibles via devfactory
|
||||
|
||||
```
|
||||
github.com/lucasdataproyects/devfactory/core # Result, Option, slice ops
|
||||
github.com/lucasdataproyects/devfactory/shell # HTTP, DB, File, Process
|
||||
github.com/lucasdataproyects/devfactory/app/finance # Yahoo, AlphaVantage, FRED
|
||||
```
|
||||
|
||||
## Cómo extender DevFactory
|
||||
|
||||
### Agregar nuevo módulo en core/ (sin efectos secundarios)
|
||||
```go
|
||||
// core/nuevo.go
|
||||
package core
|
||||
|
||||
// Funciones puras que no hacen I/O
|
||||
func MiFuncion[T any](x T) T { ... }
|
||||
```
|
||||
|
||||
### Agregar nuevo módulo en shell/ (con I/O)
|
||||
```go
|
||||
// shell/nuevo.go
|
||||
package shell
|
||||
|
||||
// Funciones que hacen I/O, retornan Result[T]
|
||||
func MiOperacion() core.Result[string] { ... }
|
||||
```
|
||||
|
||||
### Agregar nueva app/ (de alto nivel)
|
||||
```go
|
||||
// app/miapp/cliente.go
|
||||
package miapp
|
||||
|
||||
// Combina core + shell para casos de uso específicos
|
||||
type Client struct { ... }
|
||||
```
|
||||
|
||||
## Comandos
|
||||
|
||||
### Desarrollo
|
||||
```bash
|
||||
cd ~/.local_agentes/backend
|
||||
|
||||
# Ver comandos disponibles
|
||||
make help
|
||||
|
||||
# Compilar CLI
|
||||
make build
|
||||
|
||||
# Ejecutar CLI
|
||||
make run
|
||||
|
||||
# Tests
|
||||
make test
|
||||
|
||||
# Formatear código
|
||||
make fmt
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
make build # Compila bin/devfactory
|
||||
make install # Instala en ~/go/bin
|
||||
```
|
||||
|
||||
### Crear proyecto nuevo
|
||||
```bash
|
||||
# Via script
|
||||
~/.local_agentes/backend/scripts/create-project.sh mi-app /ruta
|
||||
|
||||
# Via make
|
||||
make new-project NAME=mi-app DEST=/ruta
|
||||
```
|
||||
|
||||
## Sincronización con Gitea
|
||||
|
||||
### Actualizar repo local:
|
||||
```bash
|
||||
cd ~/.local_agentes/backend
|
||||
git pull origin master
|
||||
```
|
||||
|
||||
### Subir cambios:
|
||||
```bash
|
||||
cd ~/.local_agentes/backend
|
||||
git add .
|
||||
git commit -m "feat: descripción"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
### Via Gitea MCP:
|
||||
- `get_file_content`: Leer archivos remotos
|
||||
- `create_file`: Crear archivo nuevo
|
||||
- `update_file`: Actualizar archivo existente
|
||||
|
||||
## Ejemplos de solicitudes
|
||||
|
||||
### "Crea un proyecto que use devfactory"
|
||||
1. Usar script: `~/.local_agentes/backend/scripts/create-project.sh mi-app /ruta`
|
||||
2. El proyecto ya tiene go.work configurado
|
||||
3. Importar: `import "github.com/lucasdataproyects/devfactory/core"`
|
||||
4. Cambios en devfactory se reflejan automáticamente
|
||||
|
||||
### "Necesito un cliente HTTP con retry"
|
||||
1. Buscar en `shell/http.go`
|
||||
2. Si no tiene retry, agregarlo EN LA LIBRERIA
|
||||
3. El proyecto ya puede usarlo via go.work
|
||||
|
||||
### "Quiero obtener precios de acciones"
|
||||
1. Verificar que el proyecto use go.work con devfactory
|
||||
2. Importar: `import "github.com/lucasdataproyects/devfactory/app/finance"`
|
||||
3. Usar el cliente Yahoo Finance
|
||||
|
||||
### "Dame un Result type para mi proyecto"
|
||||
1. Verificar que el proyecto use go.work con devfactory
|
||||
2. Importar: `import "github.com/lucasdataproyects/devfactory/core"`
|
||||
3. Usar `core.Ok()`, `core.Err()`, `core.Try()`
|
||||
|
||||
## Notas
|
||||
|
||||
- Rama principal: `master`
|
||||
- Arquitectura: core (puro) → shell (I/O) → app (casos de uso)
|
||||
- Siempre retorna `Result[T]` en operaciones que pueden fallar
|
||||
- Prefiere funciones genéricas cuando sea posible
|
||||
- Usa go.work para desarrollo local, no copies código
|
||||
@@ -1,510 +0,0 @@
|
||||
---
|
||||
name: build-wails
|
||||
description: Agente para crear y compilar aplicaciones desktop con Wails (Go + React). Soporta Linux, Windows y macOS.
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
mcpServers:
|
||||
- gitea:
|
||||
type: stdio
|
||||
command: gitea-mcp
|
||||
args:
|
||||
- -t
|
||||
- stdio
|
||||
- --host
|
||||
- "${GITEA_URL}"
|
||||
- --token
|
||||
- "${GITEA_TOKEN}"
|
||||
---
|
||||
|
||||
# Agente Build Wails
|
||||
|
||||
Eres un experto en Wails v2, el framework para crear aplicaciones desktop con Go backend y frontend web (React/Vue/Svelte).
|
||||
|
||||
## Tu entorno
|
||||
|
||||
- **Wails**: v2.9+
|
||||
- **Go**: 1.22+
|
||||
- **Frontend**: React 19 + TypeScript + Vite + Tailwind
|
||||
- **Librería frontend**: `@anthropic/frontend-lib` (via pnpm link)
|
||||
- **Librería backend**: DevFactory (via go.work)
|
||||
|
||||
## Capacidades
|
||||
|
||||
### Inicialización de proyectos
|
||||
- Crear proyecto Wails desde cero
|
||||
- Configurar con React + TypeScript + Vite
|
||||
- Integrar con frontend-lib y backend-lib
|
||||
|
||||
### Compilación
|
||||
- **Linux**: AMD64, ARM64
|
||||
- **Windows**: AMD64 (cross-compile desde Linux)
|
||||
- **macOS**: AMD64, ARM64 (requiere macOS o cross-compile)
|
||||
|
||||
### Desarrollo
|
||||
- Hot reload con `wails dev`
|
||||
- Debugging con DevTools
|
||||
- Bindings automáticos Go ↔ TypeScript
|
||||
|
||||
### Empaquetado
|
||||
- NSIS installer (Windows)
|
||||
- AppImage/deb/rpm (Linux)
|
||||
- DMG/pkg (macOS)
|
||||
|
||||
## Estructura de proyecto Wails
|
||||
|
||||
```
|
||||
mi-wails-app/
|
||||
├── main.go # Entry point
|
||||
├── app.go # Lógica de la aplicación (métodos expuestos al frontend)
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── go.work # Vincula devfactory localmente
|
||||
├── wails.json # Configuración de Wails
|
||||
├── build/ # Assets de build (iconos, manifests)
|
||||
│ ├── appicon.png
|
||||
│ ├── windows/
|
||||
│ │ └── icon.ico
|
||||
│ └── linux/
|
||||
│ └── icon.png
|
||||
└── frontend/ # Frontend React
|
||||
├── src/
|
||||
│ ├── App.tsx
|
||||
│ ├── main.tsx
|
||||
│ └── wailsjs/ # Bindings generados automáticamente
|
||||
│ ├── go/
|
||||
│ └── runtime/
|
||||
├── index.html
|
||||
├── package.json
|
||||
├── vite.config.ts
|
||||
└── tailwind.config.js
|
||||
```
|
||||
|
||||
## Flujo de trabajo
|
||||
|
||||
### Crear proyecto nuevo
|
||||
|
||||
1. **Verificar requisitos**:
|
||||
```bash
|
||||
wails doctor
|
||||
```
|
||||
|
||||
2. **Crear proyecto**:
|
||||
```bash
|
||||
wails init -n mi-app -t react-ts
|
||||
```
|
||||
|
||||
3. **Configurar go.work para DevFactory**:
|
||||
```bash
|
||||
cd mi-app
|
||||
go work init
|
||||
go work use . ~/.local_agentes/backend
|
||||
```
|
||||
|
||||
4. **Configurar pnpm link para frontend-lib**:
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm add @anthropic/frontend-lib@link:~/.local_agentes/frontend/frontend
|
||||
```
|
||||
|
||||
5. **Actualizar wails.json**:
|
||||
```json
|
||||
{
|
||||
"frontend:install": "pnpm install",
|
||||
"frontend:build": "pnpm build",
|
||||
"frontend:dev:watcher": "pnpm dev"
|
||||
}
|
||||
```
|
||||
|
||||
### Desarrollo
|
||||
|
||||
```bash
|
||||
# Modo desarrollo con hot reload
|
||||
wails dev
|
||||
|
||||
# Con DevTools abiertos
|
||||
wails dev -devtools
|
||||
|
||||
# Solo generar bindings
|
||||
wails generate module
|
||||
```
|
||||
|
||||
### Compilación
|
||||
|
||||
```bash
|
||||
# Linux (arquitectura actual)
|
||||
wails build
|
||||
|
||||
# Linux AMD64
|
||||
wails build -platform linux/amd64
|
||||
|
||||
# Windows (cross-compile desde Linux)
|
||||
wails build -platform windows/amd64
|
||||
|
||||
# Ambos
|
||||
wails build -platform linux/amd64,windows/amd64
|
||||
|
||||
# Con NSIS installer (Windows)
|
||||
wails build -platform windows/amd64 -nsis
|
||||
|
||||
# Con compresión UPX
|
||||
wails build -upx
|
||||
|
||||
# Producción optimizada
|
||||
wails build -clean -trimpath -ldflags="-s -w"
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
### wails.json completo
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "mi-app",
|
||||
"outputfilename": "mi-app",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "pnpm install",
|
||||
"frontend:build": "pnpm build",
|
||||
"frontend:dev:watcher": "pnpm dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"wailsjsdir": "frontend/src/wailsjs",
|
||||
"author": {
|
||||
"name": "Lucas",
|
||||
"email": "lucas@example.com"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "Mi Empresa",
|
||||
"productName": "Mi App",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright 2024",
|
||||
"comments": "Aplicación desktop con Wails"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### main.go con DevFactory
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/linux"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
app := NewApp()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Mi App",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
MinWidth: 800,
|
||||
MinHeight: 600,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
OnShutdown: app.shutdown,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
// Opciones específicas de Windows
|
||||
Windows: &windows.Options{
|
||||
WebviewIsTransparent: false,
|
||||
WindowIsTranslucent: false,
|
||||
DisableWindowIcon: false,
|
||||
},
|
||||
// Opciones específicas de Linux
|
||||
Linux: &linux.Options{
|
||||
ProgramName: "mi-app",
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### app.go con DevFactory
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lucasdataproyects/devfactory/core"
|
||||
"github.com/lucasdataproyects/devfactory/shell"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
// Cleanup
|
||||
}
|
||||
|
||||
// Método expuesto al frontend
|
||||
func (a *App) Greet(name string) string {
|
||||
return core.Ok("Hello " + name).UnwrapOr("Error")
|
||||
}
|
||||
|
||||
// Ejemplo con HTTP usando DevFactory
|
||||
func (a *App) FetchData(url string) string {
|
||||
client := shell.NewHTTPClient()
|
||||
result := client.Get(url)
|
||||
return result.UnwrapOr("Error fetching data")
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend con frontend-lib
|
||||
|
||||
```tsx
|
||||
// frontend/src/App.tsx
|
||||
import { useState } from 'react'
|
||||
import { Button, Card, Input } from '@anthropic/frontend-lib'
|
||||
import { Greet } from './wailsjs/go/main/App'
|
||||
|
||||
function App() {
|
||||
const [name, setName] = useState('')
|
||||
const [greeting, setGreeting] = useState('')
|
||||
|
||||
const handleGreet = async () => {
|
||||
const result = await Greet(name)
|
||||
setGreeting(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-8">
|
||||
<Card className="max-w-md mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Mi App Wails</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
placeholder="Tu nombre"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button onClick={handleGreet}>
|
||||
Saludar
|
||||
</Button>
|
||||
|
||||
{greeting && (
|
||||
<p className="text-foreground-muted">{greeting}</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
## Requisitos de compilación
|
||||
|
||||
### Linux (nativo)
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
# Fedora
|
||||
sudo dnf install gtk3-devel webkit2gtk4.0-devel
|
||||
|
||||
# Arch
|
||||
sudo pacman -S gtk3 webkit2gtk
|
||||
```
|
||||
|
||||
### Windows (cross-compile desde Linux)
|
||||
```bash
|
||||
# Instalar MinGW-w64
|
||||
sudo apt install gcc-mingw-w64-x86-64
|
||||
|
||||
# Instalar NSIS para instaladores
|
||||
sudo apt install nsis
|
||||
|
||||
# Variables de entorno
|
||||
export CGO_ENABLED=1
|
||||
export GOOS=windows
|
||||
export GOARCH=amd64
|
||||
export CC=x86_64-w64-mingw32-gcc
|
||||
```
|
||||
|
||||
### Docker para cross-compile
|
||||
```dockerfile
|
||||
FROM ghcr.io/nicholasjackson/wails-build:latest
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Build para todas las plataformas
|
||||
RUN wails build -platform linux/amd64
|
||||
RUN wails build -platform windows/amd64
|
||||
```
|
||||
|
||||
## Comandos
|
||||
|
||||
### Desarrollo
|
||||
```bash
|
||||
# Doctor - verificar instalación
|
||||
wails doctor
|
||||
|
||||
# Nuevo proyecto
|
||||
wails init -n nombre -t react-ts
|
||||
|
||||
# Desarrollo con hot reload
|
||||
wails dev
|
||||
|
||||
# Generar bindings
|
||||
wails generate module
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build por defecto
|
||||
wails build
|
||||
|
||||
# Build limpio
|
||||
wails build -clean
|
||||
|
||||
# Build optimizado
|
||||
wails build -clean -trimpath -ldflags="-s -w"
|
||||
|
||||
# Con UPX (compresión)
|
||||
wails build -upx
|
||||
|
||||
# Cross-compile Windows
|
||||
wails build -platform windows/amd64
|
||||
|
||||
# Con instalador NSIS
|
||||
wails build -platform windows/amd64 -nsis
|
||||
```
|
||||
|
||||
### Utilidades
|
||||
```bash
|
||||
# Actualizar Wails
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
# Ver versión
|
||||
wails version
|
||||
|
||||
# Limpiar cache
|
||||
wails build -clean
|
||||
```
|
||||
|
||||
## Integración con tus agentes
|
||||
|
||||
### Con frontend-lib
|
||||
```bash
|
||||
# En el frontend del proyecto Wails
|
||||
cd frontend
|
||||
pnpm add @anthropic/frontend-lib@link:~/.local_agentes/frontend/frontend
|
||||
|
||||
# Usar componentes
|
||||
import { Button, DataTable } from '@anthropic/frontend-lib'
|
||||
```
|
||||
|
||||
### Con backend-lib (DevFactory)
|
||||
```bash
|
||||
# En la raíz del proyecto Wails
|
||||
go work init
|
||||
go work use . ~/.local_agentes/backend
|
||||
|
||||
# Usar en app.go
|
||||
import "github.com/lucasdataproyects/devfactory/core"
|
||||
import "github.com/lucasdataproyects/devfactory/shell"
|
||||
```
|
||||
|
||||
### Con docker
|
||||
```bash
|
||||
# Usar el agente docker para containerizar el build
|
||||
# Ver: docker/templates/Dockerfile.wails
|
||||
```
|
||||
|
||||
### Con gitea
|
||||
```bash
|
||||
# Crear repo para el proyecto
|
||||
# Subir releases como attachments en Gitea
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "wails: command not found"
|
||||
```bash
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
export PATH=$PATH:$(go env GOPATH)/bin
|
||||
```
|
||||
|
||||
### Error de WebKit en Linux
|
||||
```bash
|
||||
sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
```
|
||||
|
||||
### Cross-compile Windows falla
|
||||
```bash
|
||||
# Verificar MinGW
|
||||
x86_64-w64-mingw32-gcc --version
|
||||
|
||||
# Si no existe
|
||||
sudo apt install gcc-mingw-w64-x86-64
|
||||
```
|
||||
|
||||
### Frontend no actualiza en dev
|
||||
```bash
|
||||
# Limpiar y reiniciar
|
||||
cd frontend && rm -rf node_modules && pnpm install
|
||||
wails dev
|
||||
```
|
||||
|
||||
## Ejemplos de solicitudes
|
||||
|
||||
### "Crea un proyecto Wails con mis librerías"
|
||||
1. `wails init -n mi-app -t react-ts`
|
||||
2. Configurar go.work con DevFactory
|
||||
3. Configurar pnpm link con frontend-lib
|
||||
4. Actualizar wails.json para pnpm
|
||||
5. Verificar con `wails dev`
|
||||
|
||||
### "Compila para Windows"
|
||||
1. Verificar MinGW instalado
|
||||
2. `wails build -platform windows/amd64`
|
||||
3. El ejecutable está en `build/bin/`
|
||||
|
||||
### "Crea un instalador para Windows"
|
||||
1. Verificar NSIS instalado
|
||||
2. `wails build -platform windows/amd64 -nsis`
|
||||
3. El instalador está en `build/bin/`
|
||||
|
||||
### "Compila para producción"
|
||||
1. `wails build -clean -trimpath -ldflags="-s -w" -upx`
|
||||
2. Tamaño reducido ~50%
|
||||
3. Listo para distribución
|
||||
|
||||
## Notas
|
||||
|
||||
- Wails v2 requiere Go 1.18+, recomendado 1.22+
|
||||
- El frontend se embebe en el binario via `//go:embed`
|
||||
- Los bindings Go ↔ TS se generan automáticamente
|
||||
- Cross-compile a macOS solo funciona desde macOS
|
||||
- UPX reduce tamaño pero puede causar falsos positivos en antivirus
|
||||
@@ -1,47 +0,0 @@
|
||||
# Dockerfile para compilar proyectos Wails en CI/CD
|
||||
# Soporta Linux AMD64 y Windows AMD64 (cross-compile)
|
||||
#
|
||||
# Uso:
|
||||
# docker build -t wails-builder -f Dockerfile.wails-builder .
|
||||
# docker run -v $(pwd):/app wails-builder make build-all
|
||||
|
||||
FROM golang:1.22-bookworm
|
||||
|
||||
# Evitar prompts interactivos
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Instalar dependencias de sistema
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Wails/Linux dependencies
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.0-dev \
|
||||
# Windows cross-compile
|
||||
gcc-mingw-w64-x86-64 \
|
||||
# NSIS para instaladores Windows
|
||||
nsis \
|
||||
# Node.js
|
||||
nodejs \
|
||||
npm \
|
||||
# Utilidades
|
||||
git \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instalar pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Instalar Wails
|
||||
RUN go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
# Instalar UPX para compresión (opcional)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends upx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Variables de entorno para cross-compile
|
||||
ENV PATH="/go/bin:${PATH}"
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Entry point por defecto
|
||||
CMD ["make", "build-all"]
|
||||
@@ -1,134 +0,0 @@
|
||||
# Makefile para proyecto Wails
|
||||
# Uso: make [target]
|
||||
|
||||
APP_NAME := $(shell basename $(CURDIR))
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
BUILD_DIR := build/bin
|
||||
LDFLAGS := -ldflags="-s -w -X main.Version=$(VERSION)"
|
||||
|
||||
# Colores
|
||||
GREEN := \033[0;32m
|
||||
YELLOW := \033[1;33m
|
||||
NC := \033[0m
|
||||
|
||||
.PHONY: help dev build build-linux build-windows build-all clean install doctor
|
||||
|
||||
help: ## Mostrar esta ayuda
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-20s$(NC) %s\n", $$1, $$2}'
|
||||
|
||||
# ============================================
|
||||
# DESARROLLO
|
||||
# ============================================
|
||||
|
||||
dev: ## Iniciar en modo desarrollo con hot reload
|
||||
@echo "$(GREEN)Starting dev mode...$(NC)"
|
||||
wails dev
|
||||
|
||||
dev-debug: ## Desarrollo con DevTools abiertos
|
||||
@echo "$(GREEN)Starting dev mode with DevTools...$(NC)"
|
||||
wails dev -devtools
|
||||
|
||||
generate: ## Generar bindings Go <-> TypeScript
|
||||
@echo "$(GREEN)Generating bindings...$(NC)"
|
||||
wails generate module
|
||||
|
||||
# ============================================
|
||||
# BUILD
|
||||
# ============================================
|
||||
|
||||
build: ## Build para la plataforma actual
|
||||
@echo "$(GREEN)Building for current platform...$(NC)"
|
||||
wails build $(LDFLAGS)
|
||||
|
||||
build-prod: ## Build optimizado para producción
|
||||
@echo "$(GREEN)Building optimized for production...$(NC)"
|
||||
wails build -clean -trimpath $(LDFLAGS)
|
||||
|
||||
build-linux: ## Build para Linux AMD64
|
||||
@echo "$(GREEN)Building for Linux AMD64...$(NC)"
|
||||
wails build -platform linux/amd64 -clean -trimpath $(LDFLAGS)
|
||||
|
||||
build-linux-arm: ## Build para Linux ARM64
|
||||
@echo "$(GREEN)Building for Linux ARM64...$(NC)"
|
||||
wails build -platform linux/arm64 -clean -trimpath $(LDFLAGS)
|
||||
|
||||
build-windows: ## Build para Windows AMD64 (cross-compile)
|
||||
@echo "$(GREEN)Building for Windows AMD64...$(NC)"
|
||||
@echo "$(YELLOW)Requires: gcc-mingw-w64-x86-64$(NC)"
|
||||
wails build -platform windows/amd64 -clean -trimpath $(LDFLAGS)
|
||||
|
||||
build-windows-nsis: ## Build para Windows con instalador NSIS
|
||||
@echo "$(GREEN)Building Windows installer...$(NC)"
|
||||
@echo "$(YELLOW)Requires: nsis$(NC)"
|
||||
wails build -platform windows/amd64 -nsis -clean -trimpath $(LDFLAGS)
|
||||
|
||||
build-all: build-linux build-windows ## Build para Linux y Windows
|
||||
@echo "$(GREEN)All builds completed!$(NC)"
|
||||
@ls -lah $(BUILD_DIR)/
|
||||
|
||||
build-upx: ## Build con compresión UPX
|
||||
@echo "$(GREEN)Building with UPX compression...$(NC)"
|
||||
@echo "$(YELLOW)Note: May trigger antivirus false positives$(NC)"
|
||||
wails build -upx -clean -trimpath $(LDFLAGS)
|
||||
|
||||
# ============================================
|
||||
# UTILIDADES
|
||||
# ============================================
|
||||
|
||||
clean: ## Limpiar archivos de build
|
||||
@echo "$(GREEN)Cleaning build artifacts...$(NC)"
|
||||
rm -rf $(BUILD_DIR)
|
||||
rm -rf frontend/dist
|
||||
rm -rf frontend/node_modules/.vite
|
||||
|
||||
install-deps: ## Instalar dependencias del frontend
|
||||
@echo "$(GREEN)Installing frontend dependencies...$(NC)"
|
||||
cd frontend && pnpm install
|
||||
|
||||
update-deps: ## Actualizar dependencias
|
||||
@echo "$(GREEN)Updating dependencies...$(NC)"
|
||||
go get -u ./...
|
||||
cd frontend && pnpm update
|
||||
|
||||
doctor: ## Verificar instalación de Wails
|
||||
@echo "$(GREEN)Running Wails doctor...$(NC)"
|
||||
wails doctor
|
||||
|
||||
# ============================================
|
||||
# CROSS-COMPILE SETUP
|
||||
# ============================================
|
||||
|
||||
setup-windows-cross: ## Instalar herramientas para cross-compile a Windows
|
||||
@echo "$(GREEN)Installing Windows cross-compile tools...$(NC)"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-mingw-w64-x86-64 nsis
|
||||
|
||||
setup-linux-deps: ## Instalar dependencias de Linux para Wails
|
||||
@echo "$(GREEN)Installing Linux dependencies...$(NC)"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
# ============================================
|
||||
# RELEASE
|
||||
# ============================================
|
||||
|
||||
release: build-all ## Crear release con todos los binarios
|
||||
@echo "$(GREEN)Creating release package...$(NC)"
|
||||
@mkdir -p release
|
||||
@cp $(BUILD_DIR)/$(APP_NAME) release/$(APP_NAME)-$(VERSION)-linux-amd64 2>/dev/null || true
|
||||
@cp $(BUILD_DIR)/$(APP_NAME).exe release/$(APP_NAME)-$(VERSION)-windows-amd64.exe 2>/dev/null || true
|
||||
@cd release && sha256sum * > checksums.txt
|
||||
@echo "$(GREEN)Release files:$(NC)"
|
||||
@ls -lah release/
|
||||
|
||||
# ============================================
|
||||
# INFO
|
||||
# ============================================
|
||||
|
||||
info: ## Mostrar información del proyecto
|
||||
@echo "App: $(APP_NAME)"
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Go: $(shell go version)"
|
||||
@echo "Wails: $(shell wails version 2>/dev/null || echo 'not installed')"
|
||||
@echo "Node: $(shell node --version 2>/dev/null || echo 'not installed')"
|
||||
@echo "pnpm: $(shell pnpm --version 2>/dev/null || echo 'not installed')"
|
||||
@@ -1,111 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/lucasdataproyects/devfactory/core"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct - métodos públicos se exponen al frontend
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewApp crea una nueva instancia de App
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup se llama cuando la app inicia
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// domReady se llama cuando el DOM está listo
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Inicializaciones que requieren el DOM
|
||||
}
|
||||
|
||||
// shutdown se llama cuando la app se cierra
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
// Cleanup resources
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MÉTODOS EXPUESTOS AL FRONTEND
|
||||
// ============================================
|
||||
|
||||
// Greet retorna un saludo
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, from Go!", name)
|
||||
}
|
||||
|
||||
// GetSystemInfo retorna información del sistema
|
||||
func (a *App) GetSystemInfo() map[string]string {
|
||||
return map[string]string{
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"cpus": fmt.Sprintf("%d", runtime.NumCPU()),
|
||||
"goVersion": runtime.Version(),
|
||||
}
|
||||
}
|
||||
|
||||
// OpenFileDialog abre un diálogo para seleccionar archivo
|
||||
func (a *App) OpenFileDialog() core.Result[string] {
|
||||
file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Seleccionar archivo",
|
||||
Filters: []runtime.FileFilter{
|
||||
{DisplayName: "Todos los archivos", Pattern: "*.*"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return core.Err[string](err)
|
||||
}
|
||||
return core.Ok(file)
|
||||
}
|
||||
|
||||
// SaveFileDialog abre un diálogo para guardar archivo
|
||||
func (a *App) SaveFileDialog(defaultFilename string) core.Result[string] {
|
||||
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "Guardar archivo",
|
||||
DefaultFilename: defaultFilename,
|
||||
})
|
||||
if err != nil {
|
||||
return core.Err[string](err)
|
||||
}
|
||||
return core.Ok(file)
|
||||
}
|
||||
|
||||
// ShowMessage muestra un mensaje al usuario
|
||||
func (a *App) ShowMessage(title, message string) {
|
||||
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.InfoDialog,
|
||||
Title: title,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// ShowError muestra un error al usuario
|
||||
func (a *App) ShowError(title, message string) {
|
||||
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.ErrorDialog,
|
||||
Title: title,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Confirm muestra un diálogo de confirmación
|
||||
func (a *App) Confirm(title, message string) bool {
|
||||
result, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: title,
|
||||
Message: message,
|
||||
Buttons: []string{"Sí", "No"},
|
||||
DefaultButton: "Sí",
|
||||
CancelButton: "No",
|
||||
})
|
||||
return result == "Sí"
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script para crear un nuevo proyecto Wails con DevFactory + Frontend_Library
|
||||
# Uso: ./create-wails-project.sh <nombre-proyecto> [directorio-destino]
|
||||
|
||||
set -e
|
||||
|
||||
# ============================================
|
||||
# CONFIGURACIÓN
|
||||
# ============================================
|
||||
PROJECT_NAME="${1:-mi-wails-app}"
|
||||
DEST_DIR="${2:-.}"
|
||||
FULL_PATH="$DEST_DIR/$PROJECT_NAME"
|
||||
|
||||
DEVFACTORY_PATH="$HOME/.local_agentes/backend"
|
||||
FRONTEND_LIB_PATH="$HOME/.local_agentes/frontend/frontend"
|
||||
TEMPLATES_PATH="$(dirname "$0")"
|
||||
|
||||
# Colores
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
||||
info() { echo -e "${BLUE}[i]${NC} $1"; }
|
||||
|
||||
# ============================================
|
||||
# VALIDACIONES
|
||||
# ============================================
|
||||
info "Verificando requisitos..."
|
||||
|
||||
if ! command -v wails &> /dev/null; then
|
||||
error "Wails no está instalado. Ejecuta: go install github.com/wailsapp/wails/v2/cmd/wails@latest"
|
||||
fi
|
||||
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
error "pnpm no está instalado. Ejecuta: npm install -g pnpm"
|
||||
fi
|
||||
|
||||
if [ -d "$FULL_PATH" ]; then
|
||||
error "El directorio $FULL_PATH ya existe"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# CREAR PROYECTO
|
||||
# ============================================
|
||||
info "Creando proyecto Wails: $PROJECT_NAME"
|
||||
|
||||
# Crear proyecto base con Wails
|
||||
wails init -n "$PROJECT_NAME" -t react-ts -d "$DEST_DIR"
|
||||
|
||||
cd "$FULL_PATH"
|
||||
|
||||
log "Proyecto base creado"
|
||||
|
||||
# ============================================
|
||||
# CONFIGURAR GO.WORK (DevFactory)
|
||||
# ============================================
|
||||
info "Configurando go.work para DevFactory..."
|
||||
|
||||
if [ -d "$DEVFACTORY_PATH" ]; then
|
||||
go work init
|
||||
go work use . "$DEVFACTORY_PATH"
|
||||
log "go.work configurado con DevFactory"
|
||||
else
|
||||
warn "DevFactory no encontrado en $DEVFACTORY_PATH"
|
||||
warn "Configúralo manualmente con: go work use ~/.local_agentes/backend"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# CONFIGURAR FRONTEND
|
||||
# ============================================
|
||||
info "Configurando frontend..."
|
||||
|
||||
cd frontend
|
||||
|
||||
# Cambiar a pnpm
|
||||
rm -f package-lock.json yarn.lock
|
||||
pnpm install
|
||||
|
||||
# Agregar frontend-lib si existe
|
||||
if [ -d "$FRONTEND_LIB_PATH" ]; then
|
||||
pnpm add "@anthropic/frontend-lib@link:$FRONTEND_LIB_PATH"
|
||||
log "frontend-lib vinculado"
|
||||
else
|
||||
warn "Frontend_Library no encontrado en $FRONTEND_LIB_PATH"
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
||||
# ============================================
|
||||
# ACTUALIZAR wails.json
|
||||
# ============================================
|
||||
info "Actualizando wails.json..."
|
||||
|
||||
cat > wails.json << EOF
|
||||
{
|
||||
"\$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "$PROJECT_NAME",
|
||||
"outputfilename": "$PROJECT_NAME",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "pnpm install",
|
||||
"frontend:build": "pnpm build",
|
||||
"frontend:dev:watcher": "pnpm dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"wailsjsdir": "frontend/src/wailsjs",
|
||||
"author": {
|
||||
"name": "$(git config user.name 2>/dev/null || echo 'Developer')",
|
||||
"email": "$(git config user.email 2>/dev/null || echo 'dev@example.com')"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "$PROJECT_NAME",
|
||||
"productName": "$PROJECT_NAME",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright $(date +%Y)",
|
||||
"comments": "Built with Wails"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
log "wails.json actualizado para pnpm"
|
||||
|
||||
# ============================================
|
||||
# COPIAR MAKEFILE
|
||||
# ============================================
|
||||
if [ -f "$TEMPLATES_PATH/Makefile" ]; then
|
||||
cp "$TEMPLATES_PATH/Makefile" ./Makefile
|
||||
log "Makefile copiado"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# CREAR .gitignore
|
||||
# ============================================
|
||||
cat > .gitignore << 'EOF'
|
||||
# Build
|
||||
build/bin/
|
||||
frontend/dist/
|
||||
|
||||
# Dependencies
|
||||
frontend/node_modules/
|
||||
|
||||
# Go
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
vendor/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Wails
|
||||
frontend/src/wailsjs/
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
EOF
|
||||
|
||||
log ".gitignore creado"
|
||||
|
||||
# ============================================
|
||||
# INICIALIZAR GIT
|
||||
# ============================================
|
||||
if [ ! -d ".git" ]; then
|
||||
git init
|
||||
git add .
|
||||
git commit -m "feat: initial Wails project with DevFactory + Frontend_Library"
|
||||
log "Repositorio Git inicializado"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# RESUMEN
|
||||
# ============================================
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Proyecto Wails creado exitosamente!${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${BLUE}Directorio:${NC} $FULL_PATH"
|
||||
echo ""
|
||||
echo -e " ${BLUE}Comandos disponibles:${NC}"
|
||||
echo -e " make dev - Desarrollo con hot reload"
|
||||
echo -e " make build - Build para plataforma actual"
|
||||
echo -e " make build-linux - Build para Linux"
|
||||
echo -e " make build-windows - Build para Windows"
|
||||
echo -e " make build-all - Build para todas las plataformas"
|
||||
echo -e " make help - Ver todos los comandos"
|
||||
echo ""
|
||||
echo -e " ${BLUE}Próximos pasos:${NC}"
|
||||
echo -e " cd $FULL_PATH"
|
||||
echo -e " make dev"
|
||||
echo ""
|
||||
|
||||
# Verificar wails doctor
|
||||
info "Ejecutando wails doctor..."
|
||||
wails doctor || warn "Algunos requisitos pueden faltar. Revisa la salida anterior."
|
||||
@@ -1,8 +0,0 @@
|
||||
go 1.22
|
||||
|
||||
use (
|
||||
.
|
||||
// DevFactory - librería Go funcional
|
||||
// Descomentar y ajustar path si usas DevFactory
|
||||
// ~/.local_agentes/backend
|
||||
)
|
||||
@@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/linux"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
app := NewApp()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: "{{APP_TITLE}}",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
MinWidth: 800,
|
||||
MinHeight: 600,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 15, G: 23, B: 42, A: 1}, // slate-900
|
||||
OnStartup: app.startup,
|
||||
OnShutdown: app.shutdown,
|
||||
OnDomReady: app.domReady,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
// Windows options
|
||||
Windows: &windows.Options{
|
||||
WebviewIsTransparent: false,
|
||||
WindowIsTranslucent: false,
|
||||
DisableWindowIcon: false,
|
||||
DisableFramelessWindowDecorations: false,
|
||||
WebviewUserDataPath: "",
|
||||
Theme: windows.SystemDefault,
|
||||
},
|
||||
// Linux options
|
||||
Linux: &linux.Options{
|
||||
ProgramName: "{{APP_NAME}}",
|
||||
WebviewGpuPolicy: linux.WebviewGpuPolicyAlways,
|
||||
WindowIsTranslucent: false,
|
||||
},
|
||||
// macOS options
|
||||
Mac: &mac.Options{
|
||||
TitleBar: &mac.TitleBar{
|
||||
TitlebarAppearsTransparent: true,
|
||||
HideTitle: false,
|
||||
HideTitleBar: false,
|
||||
FullSizeContent: false,
|
||||
UseToolbar: false,
|
||||
HideToolbarSeparator: true,
|
||||
},
|
||||
About: &mac.AboutInfo{
|
||||
Title: "{{APP_TITLE}}",
|
||||
Message: "Built with Wails + React",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "{{APP_NAME}}",
|
||||
"outputfilename": "{{APP_NAME}}",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "pnpm install",
|
||||
"frontend:build": "pnpm build",
|
||||
"frontend:dev:watcher": "pnpm dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"wailsjsdir": "frontend/src/wailsjs",
|
||||
"author": {
|
||||
"name": "{{AUTHOR_NAME}}",
|
||||
"email": "{{AUTHOR_EMAIL}}"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "{{COMPANY_NAME}}",
|
||||
"productName": "{{PRODUCT_NAME}}",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright {{YEAR}}",
|
||||
"comments": "Built with Wails"
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
name: db-reader
|
||||
description: Agente especializado en bases de datos SQLite y DuckDB. Puede crear, consultar, insertar y analizar datos.
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# Agente DB Reader
|
||||
|
||||
Eres un experto en bases de datos SQLite y DuckDB. Tu rol es ayudar al usuario a:
|
||||
|
||||
## Capacidades
|
||||
|
||||
### SQLite
|
||||
- Crear bases de datos y tablas
|
||||
- Insertar, actualizar y eliminar datos
|
||||
- Ejecutar queries SELECT complejos
|
||||
- Crear índices y optimizar consultas
|
||||
- Exportar datos a CSV/JSON
|
||||
|
||||
### DuckDB
|
||||
- Análisis de datos con SQL analítico
|
||||
- Importar datos desde CSV, Parquet, JSON
|
||||
- Ejecutar queries OLAP eficientes
|
||||
- Window functions y CTEs
|
||||
- Exportar resultados
|
||||
|
||||
## Flujo de trabajo
|
||||
|
||||
1. **Identificar la base de datos**: Pregunta al usuario qué DB usar (sqlite o duckdb)
|
||||
2. **Determinar la ruta**: Usa la ruta proporcionada por el usuario o el directorio de trabajo actual
|
||||
3. **Crear la base de datos**: Usa Python con los módulos `sqlite3` o `duckdb`
|
||||
4. **Ejecutar operaciones**: CREATE, INSERT, SELECT, UPDATE, DELETE
|
||||
5. **Mostrar resultados**: Formatea los resultados de forma legible
|
||||
|
||||
## REGLAS CRÍTICAS - RUTAS DE ARCHIVOS
|
||||
|
||||
**NUNCA** crees archivos o directorios con nombres que contengan:
|
||||
- Variables de entorno: `${VAR}`, `$VAR`, `${VAR:-default}`
|
||||
- Caracteres especiales: `{`, `}`, `$`
|
||||
|
||||
**SIEMPRE**:
|
||||
- Usa rutas absolutas expandidas (ej: `/home/user/proyecto/data.duckdb`)
|
||||
- O rutas relativas simples sin variables (ej: `./data.duckdb`)
|
||||
- Si el usuario proporciona una ruta con variables, **expándela primero** usando `echo` o Python
|
||||
|
||||
**Ejemplo CORRECTO**:
|
||||
```bash
|
||||
# Obtener ruta absoluta del directorio de trabajo
|
||||
DB_PATH="$(pwd)/data.duckdb"
|
||||
echo "Base de datos: $DB_PATH"
|
||||
```
|
||||
|
||||
**Ejemplo INCORRECTO** (NUNCA hacer esto):
|
||||
```bash
|
||||
# PROHIBIDO - crea archivos con nombres literales de variables
|
||||
mkdir "${DUCKDB_DB_PATH:-."
|
||||
touch "${SQLITE_DB_PATH:-./data.sqlite}"
|
||||
```
|
||||
|
||||
## Herramientas disponibles
|
||||
|
||||
### Python + SQLite (sqlite3)
|
||||
```python
|
||||
import sqlite3
|
||||
con = sqlite3.connect('/ruta/absoluta/data.sqlite')
|
||||
cursor = con.cursor()
|
||||
cursor.execute("SELECT * FROM tabla")
|
||||
results = cursor.fetchall()
|
||||
con.close()
|
||||
```
|
||||
|
||||
### Python + DuckDB
|
||||
```python
|
||||
import duckdb
|
||||
con = duckdb.connect('/ruta/absoluta/data.duckdb')
|
||||
results = con.execute("SELECT * FROM tabla").fetchall()
|
||||
con.close()
|
||||
```
|
||||
|
||||
### Instalación de dependencias
|
||||
```bash
|
||||
pip install duckdb # DuckDB (sqlite3 viene incluido en Python)
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Siempre mostrar el schema antes de operar
|
||||
- Confirmar operaciones destructivas (DROP, DELETE)
|
||||
- Formatear resultados en tablas markdown
|
||||
- Explicar queries complejos
|
||||
|
||||
## Ejemplos de uso
|
||||
|
||||
### Crear tabla SQLite
|
||||
```sql
|
||||
CREATE TABLE usuarios (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nombre TEXT NOT NULL,
|
||||
email TEXT UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Importar CSV en DuckDB
|
||||
```sql
|
||||
CREATE TABLE ventas AS
|
||||
SELECT * FROM read_csv_auto('ventas.csv');
|
||||
```
|
||||
|
||||
### Análisis con DuckDB
|
||||
```sql
|
||||
SELECT
|
||||
DATE_TRUNC('month', fecha) as mes,
|
||||
SUM(total) as ventas_totales,
|
||||
COUNT(*) as num_transacciones
|
||||
FROM ventas
|
||||
GROUP BY 1
|
||||
ORDER BY 1;
|
||||
```
|
||||
|
||||
## Rutas de bases de datos
|
||||
|
||||
Por defecto, crear las bases de datos en el directorio de trabajo actual:
|
||||
- SQLite: `{directorio_trabajo}/data.sqlite`
|
||||
- DuckDB: `{directorio_trabajo}/data.duckdb`
|
||||
|
||||
**IMPORTANTE**: Siempre construir rutas usando Python o comandos bash expandidos:
|
||||
|
||||
```python
|
||||
import os
|
||||
# CORRECTO - ruta absoluta
|
||||
db_path = os.path.join(os.getcwd(), "data.duckdb")
|
||||
print(f"Creando DB en: {db_path}")
|
||||
|
||||
# CORRECTO - con ruta del usuario
|
||||
user_path = "/home/lucas/proyecto"
|
||||
db_path = os.path.join(user_path, "data.duckdb")
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- DuckDB es mejor para análisis de datos grandes (OLAP)
|
||||
- SQLite es mejor para datos transaccionales (OLTP)
|
||||
- Ambos soportan SQL estándar
|
||||
- **NUNCA** usar strings con `${...}` como nombres de archivo
|
||||
@@ -1,453 +0,0 @@
|
||||
---
|
||||
name: docker
|
||||
description: Agente para containerizar aplicaciones - genera Dockerfiles, docker-compose, y gestiona builds/deployments
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
# Agente Docker
|
||||
|
||||
Eres un experto en containerización con Docker. Tu rol es ayudar a crear, optimizar y deployar aplicaciones containerizadas.
|
||||
|
||||
## Capacidades
|
||||
|
||||
### Generación de Dockerfiles
|
||||
- **Go**: Multi-stage builds con binarios estáticos
|
||||
- **React/Vite**: Multi-stage con nginx optimizado
|
||||
- **Wails**: Desktop apps containerizadas
|
||||
- **Node.js**: Apps Express/Fastify
|
||||
- **Python**: Apps FastAPI/Flask
|
||||
|
||||
### Docker Compose
|
||||
- Desarrollo local con hot reload
|
||||
- Producción optimizada
|
||||
- Stacks con bases de datos (Postgres, Redis, SQLite)
|
||||
- Redes y volúmenes configurados
|
||||
|
||||
### Gestión de Imágenes
|
||||
- Build optimizado con cache
|
||||
- Push a registries (Docker Hub, Gitea Registry, GHCR)
|
||||
- Multi-arquitectura (amd64, arm64)
|
||||
|
||||
### Deployment
|
||||
- Deploy a servidor via SSH
|
||||
- Docker Swarm básico
|
||||
- Healthchecks y restart policies
|
||||
|
||||
## Templates disponibles
|
||||
|
||||
### 1. Go Backend (DevFactory)
|
||||
|
||||
```dockerfile
|
||||
# === BUILD STAGE ===
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencias primero (cache)
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Código fuente
|
||||
COPY . .
|
||||
|
||||
# Build estático
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server
|
||||
|
||||
# === RUNTIME STAGE ===
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/server .
|
||||
|
||||
# Usuario no-root
|
||||
RUN adduser -D -g '' appuser
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
ENTRYPOINT ["./server"]
|
||||
```
|
||||
|
||||
### 2. React/Vite Frontend
|
||||
|
||||
```dockerfile
|
||||
# === BUILD STAGE ===
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencias primero (cache)
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN corepack enable && pnpm install --frozen-lockfile
|
||||
|
||||
# Código fuente
|
||||
COPY . .
|
||||
|
||||
# Build producción
|
||||
RUN pnpm build
|
||||
|
||||
# === RUNTIME STAGE ===
|
||||
FROM nginx:alpine
|
||||
|
||||
# Configuración nginx optimizada
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Archivos estáticos
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Usuario no-root
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
### 3. Fullstack (Go + React)
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:80"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgres://user:pass@db:5432/app?sslmode=disable
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: pass
|
||||
POSTGRES_DB: app
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U user -d app"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
### 4. Nginx config para SPA
|
||||
|
||||
```nginx
|
||||
# nginx.conf
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logs
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# Performance
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/javascript application/json application/xml;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# SPA routing
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API proxy (opcional)
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Cache para assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Flujo de trabajo
|
||||
|
||||
### Cuando te pidan containerizar un proyecto:
|
||||
|
||||
1. **Detectar tipo de proyecto**:
|
||||
```bash
|
||||
# Go?
|
||||
ls go.mod
|
||||
# Node/React?
|
||||
ls package.json
|
||||
# Python?
|
||||
ls requirements.txt pyproject.toml
|
||||
```
|
||||
|
||||
2. **Analizar estructura**:
|
||||
- Punto de entrada (main.go, src/main.tsx, etc.)
|
||||
- Dependencias
|
||||
- Variables de entorno necesarias
|
||||
- Puertos expuestos
|
||||
|
||||
3. **Generar archivos**:
|
||||
- `Dockerfile` (multi-stage optimizado)
|
||||
- `docker-compose.yml` (si hay servicios)
|
||||
- `.dockerignore` (siempre)
|
||||
- `nginx.conf` (si es frontend)
|
||||
|
||||
4. **Validar**:
|
||||
```bash
|
||||
docker build -t app:test .
|
||||
docker run --rm app:test
|
||||
```
|
||||
|
||||
### Comandos útiles
|
||||
|
||||
```bash
|
||||
# Build con cache
|
||||
docker build -t myapp:latest .
|
||||
|
||||
# Build sin cache
|
||||
docker build --no-cache -t myapp:latest .
|
||||
|
||||
# Build multi-plataforma
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
|
||||
|
||||
# Ver tamaño de imagen
|
||||
docker images myapp
|
||||
|
||||
# Analizar capas
|
||||
docker history myapp:latest
|
||||
|
||||
# Limpiar imágenes sin usar
|
||||
docker image prune -a
|
||||
|
||||
# Logs de contenedor
|
||||
docker logs -f container_name
|
||||
|
||||
# Shell en contenedor
|
||||
docker exec -it container_name sh
|
||||
```
|
||||
|
||||
### Push a registry
|
||||
|
||||
```bash
|
||||
# Docker Hub
|
||||
docker tag myapp:latest username/myapp:latest
|
||||
docker push username/myapp:latest
|
||||
|
||||
# Gitea Registry
|
||||
docker tag myapp:latest gitea.example.com/user/myapp:latest
|
||||
docker login gitea.example.com
|
||||
docker push gitea.example.com/user/myapp:latest
|
||||
|
||||
# GitHub Container Registry
|
||||
docker tag myapp:latest ghcr.io/username/myapp:latest
|
||||
echo $GITHUB_TOKEN | docker login ghcr.io -u username --password-stdin
|
||||
docker push ghcr.io/username/myapp:latest
|
||||
```
|
||||
|
||||
## Patrones de optimización
|
||||
|
||||
### 1. Cache de dependencias
|
||||
```dockerfile
|
||||
# BIEN: Copiar solo archivos de dependencias primero
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN go build
|
||||
|
||||
# MAL: Copiar todo junto (invalida cache siempre)
|
||||
COPY . .
|
||||
RUN go mod download && go build
|
||||
```
|
||||
|
||||
### 2. Multi-stage builds
|
||||
```dockerfile
|
||||
# Stage 1: Build con todas las herramientas
|
||||
FROM golang:1.22 AS builder
|
||||
# ... build ...
|
||||
|
||||
# Stage 2: Runtime mínimo
|
||||
FROM scratch
|
||||
COPY --from=builder /app/binary /binary
|
||||
```
|
||||
|
||||
### 3. Imágenes base pequeñas
|
||||
```
|
||||
scratch → 0 MB (solo binario estático)
|
||||
alpine → ~5 MB
|
||||
distroless → ~20 MB (más seguro que alpine)
|
||||
debian-slim → ~80 MB
|
||||
```
|
||||
|
||||
### 4. .dockerignore
|
||||
```
|
||||
# .dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
node_modules
|
||||
*.md
|
||||
.env*
|
||||
.vscode
|
||||
.idea
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
```
|
||||
|
||||
## Integración con tus agentes
|
||||
|
||||
### Con backend-lib (DevFactory)
|
||||
```bash
|
||||
# El proyecto Go usa devfactory via go.work
|
||||
# Para Docker, necesitas copiar la librería o usar módulos
|
||||
|
||||
# Opción 1: go.work en build (recomendado para dev)
|
||||
COPY go.work go.work.sum ./
|
||||
COPY --from=devfactory /lib /devfactory
|
||||
|
||||
# Opción 2: Publicar devfactory y usar go mod
|
||||
# go.mod: require github.com/lucasdataproyects/devfactory v1.0.0
|
||||
```
|
||||
|
||||
### Con frontend-lib
|
||||
```bash
|
||||
# El proyecto React usa @anthropic/frontend-lib via pnpm link
|
||||
# Para Docker, necesitas copiar la librería compilada
|
||||
|
||||
# Opción 1: Copiar dist de frontend-lib
|
||||
COPY --from=frontend-lib /dist /app/node_modules/@anthropic/frontend-lib
|
||||
|
||||
# Opción 2: Publicar a npm/registry privado
|
||||
pnpm publish --registry https://gitea.example.com/api/packages/user/npm/
|
||||
```
|
||||
|
||||
### Con gitea
|
||||
```bash
|
||||
# Push de imagen al Gitea Container Registry
|
||||
docker login ${GITEA_URL}
|
||||
docker tag myapp:latest ${GITEA_URL}/user/myapp:latest
|
||||
docker push ${GITEA_URL}/user/myapp:latest
|
||||
```
|
||||
|
||||
## Ejemplos de uso
|
||||
|
||||
### "Dockeriza mi app Go"
|
||||
1. Detectar estructura del proyecto
|
||||
2. Generar Dockerfile multi-stage con Alpine
|
||||
3. Generar .dockerignore
|
||||
4. Build y test local
|
||||
|
||||
### "Crea un compose para desarrollo"
|
||||
1. Analizar servicios necesarios (DB, cache, etc.)
|
||||
2. Generar docker-compose.dev.yml con hot reload
|
||||
3. Configurar volúmenes para código local
|
||||
4. Agregar healthchecks
|
||||
|
||||
### "Prepara mi app para producción"
|
||||
1. Optimizar Dockerfile (multi-stage, alpine/scratch)
|
||||
2. Generar docker-compose.prod.yml
|
||||
3. Configurar healthchecks y restart policies
|
||||
4. Generar nginx.conf si hay frontend
|
||||
|
||||
### "Deploy a mi servidor"
|
||||
1. Build de imagen local
|
||||
2. Push a registry (Gitea/Docker Hub)
|
||||
3. Script de deploy via SSH
|
||||
4. Verificar que el servicio está healthy
|
||||
|
||||
## Variables de entorno comunes
|
||||
|
||||
```yaml
|
||||
# Backend
|
||||
DATABASE_URL: postgres://user:pass@db:5432/app
|
||||
REDIS_URL: redis://redis:6379
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
PORT: 8080
|
||||
|
||||
# Frontend
|
||||
VITE_API_URL: /api
|
||||
VITE_WS_URL: ws://localhost:8080/ws
|
||||
|
||||
# Docker
|
||||
COMPOSE_PROJECT_NAME: myapp
|
||||
DOCKER_BUILDKIT: 1
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Siempre usar multi-stage builds para reducir tamaño
|
||||
- Nunca incluir secretos en la imagen (usar env vars o secrets)
|
||||
- Healthchecks son obligatorios para producción
|
||||
- Usuario non-root siempre que sea posible
|
||||
- .dockerignore es tan importante como Dockerfile
|
||||
@@ -1,85 +0,0 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.docker
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
LICENSE
|
||||
docs/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Node.js
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
build
|
||||
out
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Go
|
||||
bin/
|
||||
vendor/
|
||||
*.test
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
# Test
|
||||
__tests__
|
||||
*.test.ts
|
||||
*.test.tsx
|
||||
*.spec.ts
|
||||
*.spec.tsx
|
||||
e2e/
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Temp
|
||||
tmp
|
||||
temp
|
||||
.tmp
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
.travis.yml
|
||||
Jenkinsfile
|
||||
|
||||
# Misc
|
||||
Makefile
|
||||
*.sh
|
||||
!entrypoint.sh
|
||||
@@ -1,49 +0,0 @@
|
||||
# === BUILD STAGE ===
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias de compilación si necesario
|
||||
# RUN apk add --no-cache gcc musl-dev
|
||||
|
||||
# Dependencias primero (mejor cache)
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download && go mod verify
|
||||
|
||||
# Código fuente
|
||||
COPY . .
|
||||
|
||||
# Build binario estático
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -ldflags="-w -s -extldflags '-static'" \
|
||||
-o /app/server ./cmd/server
|
||||
|
||||
# === RUNTIME STAGE ===
|
||||
FROM alpine:3.19
|
||||
|
||||
# Certificados SSL y timezone
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar binario
|
||||
COPY --from=builder /app/server .
|
||||
|
||||
# Copiar archivos estáticos/config si necesario
|
||||
# COPY --from=builder /app/configs ./configs
|
||||
# COPY --from=builder /app/migrations ./migrations
|
||||
|
||||
# Usuario no-root por seguridad
|
||||
RUN addgroup -g 1001 -S appgroup && \
|
||||
adduser -u 1001 -S appuser -G appgroup
|
||||
USER appuser
|
||||
|
||||
# Puerto de la aplicación
|
||||
EXPOSE 8080
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# Punto de entrada
|
||||
ENTRYPOINT ["./server"]
|
||||
@@ -1,53 +0,0 @@
|
||||
# === BUILD STAGE ===
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Habilitar pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
# Dependencias primero (mejor cache)
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Código fuente
|
||||
COPY . .
|
||||
|
||||
# Variables de entorno para build (pueden ser sobreescritas)
|
||||
ARG VITE_API_URL=/api
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
|
||||
# Build de producción
|
||||
RUN pnpm build
|
||||
|
||||
# === RUNTIME STAGE ===
|
||||
FROM nginx:1.25-alpine
|
||||
|
||||
# Remover config por defecto
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copiar configuración nginx
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copiar archivos estáticos
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Permisos correctos
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html && \
|
||||
chown -R nginx:nginx /var/cache/nginx && \
|
||||
chown -R nginx:nginx /var/log/nginx && \
|
||||
touch /var/run/nginx.pid && \
|
||||
chown -R nginx:nginx /var/run/nginx.pid
|
||||
|
||||
# Usuario no-root
|
||||
USER nginx
|
||||
|
||||
# Puerto
|
||||
EXPOSE 80
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1
|
||||
|
||||
# Iniciar nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script de deploy para servidor remoto
|
||||
# Uso: ./deploy.sh [production|staging]
|
||||
|
||||
set -e
|
||||
|
||||
# ============================================
|
||||
# CONFIGURACIÓN
|
||||
# ============================================
|
||||
ENV=${1:-production}
|
||||
REGISTRY="${REGISTRY:-ghcr.io}"
|
||||
PROJECT="${PROJECT:-myapp}"
|
||||
VERSION="${VERSION:-latest}"
|
||||
|
||||
# Servidor remoto
|
||||
REMOTE_USER="${REMOTE_USER:-deploy}"
|
||||
REMOTE_HOST="${REMOTE_HOST:-server.example.com}"
|
||||
REMOTE_PATH="${REMOTE_PATH:-/opt/$PROJECT}"
|
||||
|
||||
# Colores
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||
|
||||
# ============================================
|
||||
# VALIDACIONES
|
||||
# ============================================
|
||||
log "Validando configuración..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
error "Docker no está instalado"
|
||||
fi
|
||||
|
||||
if [ -z "$REMOTE_HOST" ] || [ "$REMOTE_HOST" = "server.example.com" ]; then
|
||||
error "Configura REMOTE_HOST antes de ejecutar"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# BUILD
|
||||
# ============================================
|
||||
log "Building imágenes para $ENV..."
|
||||
|
||||
# Build con buildkit para mejor cache
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
docker build -t $REGISTRY/$PROJECT-frontend:$VERSION ./frontend
|
||||
docker build -t $REGISTRY/$PROJECT-backend:$VERSION ./backend
|
||||
|
||||
log "Imágenes construidas:"
|
||||
docker images | grep $PROJECT
|
||||
|
||||
# ============================================
|
||||
# PUSH A REGISTRY
|
||||
# ============================================
|
||||
log "Pushing imágenes a $REGISTRY..."
|
||||
|
||||
docker push $REGISTRY/$PROJECT-frontend:$VERSION
|
||||
docker push $REGISTRY/$PROJECT-backend:$VERSION
|
||||
|
||||
log "Imágenes subidas correctamente"
|
||||
|
||||
# ============================================
|
||||
# DEPLOY A SERVIDOR
|
||||
# ============================================
|
||||
log "Desplegando a $REMOTE_HOST..."
|
||||
|
||||
# Copiar docker-compose si no existe
|
||||
ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_PATH"
|
||||
scp docker-compose.yml $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/
|
||||
|
||||
# Copiar .env si existe
|
||||
if [ -f ".env.$ENV" ]; then
|
||||
scp .env.$ENV $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/.env
|
||||
fi
|
||||
|
||||
# Deploy remoto
|
||||
ssh $REMOTE_USER@$REMOTE_HOST << EOF
|
||||
cd $REMOTE_PATH
|
||||
|
||||
# Pull nuevas imágenes
|
||||
docker compose pull
|
||||
|
||||
# Restart servicios
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
# Limpiar imágenes antiguas
|
||||
docker image prune -f
|
||||
|
||||
# Verificar estado
|
||||
docker compose ps
|
||||
EOF
|
||||
|
||||
# ============================================
|
||||
# VERIFICACIÓN
|
||||
# ============================================
|
||||
log "Verificando deploy..."
|
||||
|
||||
sleep 5
|
||||
|
||||
# Healthcheck
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://$REMOTE_HOST/health || echo "000")
|
||||
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
log "Deploy exitoso! Servidor respondiendo correctamente."
|
||||
else
|
||||
warn "Servidor respondió con código: $HTTP_STATUS"
|
||||
warn "Verifica los logs: ssh $REMOTE_USER@$REMOTE_HOST 'docker compose -f $REMOTE_PATH/docker-compose.yml logs'"
|
||||
fi
|
||||
|
||||
log "Deploy completado para $ENV"
|
||||
@@ -1,100 +0,0 @@
|
||||
# Docker Compose para DESARROLLO con hot reload
|
||||
# Uso: docker compose -f docker-compose.dev.yml up
|
||||
|
||||
services:
|
||||
# ============================================
|
||||
# FRONTEND (Vite dev server con hot reload)
|
||||
# ============================================
|
||||
frontend:
|
||||
image: node:22-alpine
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-frontend-dev
|
||||
working_dir: /app
|
||||
command: sh -c "corepack enable && pnpm install && pnpm dev --host 0.0.0.0"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- frontend_node_modules:/app/node_modules
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-5173}:5173"
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:${BACKEND_PORT:-8080}
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
# ============================================
|
||||
# BACKEND (Go con air para hot reload)
|
||||
# ============================================
|
||||
backend:
|
||||
image: cosmtrek/air:latest
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-backend-dev
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- go_mod_cache:/go/pkg/mod
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8080}:8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgres://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-secret}@db:5432/${POSTGRES_DB:-app}?sslmode=disable
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- ENV=development
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
# ============================================
|
||||
# DATABASE (PostgreSQL)
|
||||
# ============================================
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-db-dev
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secret}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
volumes:
|
||||
- postgres_data_dev:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ============================================
|
||||
# CACHE (Redis)
|
||||
# ============================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-redis-dev
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
# ============================================
|
||||
# ADMINER (UI para DB - opcional)
|
||||
# ============================================
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-adminer
|
||||
ports:
|
||||
- "8081:8080"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data_dev:
|
||||
frontend_node_modules:
|
||||
go_mod_cache:
|
||||
@@ -1,119 +0,0 @@
|
||||
# Docker Compose para stack completo: Frontend + Backend + DB
|
||||
# Uso: docker compose -f docker-compose.yml up -d
|
||||
|
||||
services:
|
||||
# ============================================
|
||||
# FRONTEND (React/Vite + Nginx)
|
||||
# ============================================
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VITE_API_URL: /api
|
||||
image: ${COMPOSE_PROJECT_NAME:-myapp}-frontend:latest
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3000}:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# ============================================
|
||||
# BACKEND (Go)
|
||||
# ============================================
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
image: ${COMPOSE_PROJECT_NAME:-myapp}-backend:latest
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8080}:8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgres://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-secret}@db:5432/${POSTGRES_DB:-app}?sslmode=disable
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- JWT_SECRET=${JWT_SECRET:-change-me-in-production}
|
||||
- ENV=${ENV:-production}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# ============================================
|
||||
# DATABASE (PostgreSQL)
|
||||
# ============================================
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secret}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
# Opcional: scripts de inicialización
|
||||
# - ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-app}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ============================================
|
||||
# CACHE (Redis)
|
||||
# ============================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-myapp}-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ============================================
|
||||
# NETWORKS
|
||||
# ============================================
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
# ============================================
|
||||
# VOLUMES
|
||||
# ============================================
|
||||
volumes:
|
||||
postgres_data:
|
||||
name: ${COMPOSE_PROJECT_NAME:-myapp}_postgres_data
|
||||
redis_data:
|
||||
name: ${COMPOSE_PROJECT_NAME:-myapp}_redis_data
|
||||
@@ -1,118 +0,0 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# Performance
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Buffer sizes
|
||||
client_body_buffer_size 10K;
|
||||
client_header_buffer_size 1k;
|
||||
client_max_body_size 8m;
|
||||
large_client_header_buffers 4 4k;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/json
|
||||
application/xml
|
||||
application/rss+xml
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# SPA: todas las rutas van a index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Proxy para API backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 90s;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
location /ws {
|
||||
proxy_pass http://backend:8080/ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# Cache agresivo para assets estáticos
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# No cachear index.html ni manifest
|
||||
location ~* \.(html|json)$ {
|
||||
expires -1;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Ocultar versión de nginx
|
||||
server_tokens off;
|
||||
|
||||
# Páginas de error
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
---
|
||||
name: frontend-lib
|
||||
description: Agente que gestiona Frontend_Library - componentes React/TypeScript reutilizables para Wails y webapps. Trabaja en ~/.local_agentes/frontend y sincroniza con Gitea.
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
mcpServers:
|
||||
- gitea:
|
||||
type: stdio
|
||||
command: gitea-mcp
|
||||
args:
|
||||
- -t
|
||||
- stdio
|
||||
- --host
|
||||
- "${GITEA_URL}"
|
||||
- --token
|
||||
- "${GITEA_TOKEN}"
|
||||
---
|
||||
|
||||
# Agente Frontend Library
|
||||
|
||||
Eres el guardian de **Frontend_Library**, una libreria de componentes React/TypeScript con shadcn/ui y Tailwind para crear interfaces reutilizables en apps Wails y webapps.
|
||||
|
||||
## Tu entorno
|
||||
|
||||
- **Repositorio Gitea**: `Bl4cksmith/Frontend_Library`
|
||||
- **Carpeta local**: `~/.local_agentes/frontend`
|
||||
- **Stack**: React 19, TypeScript, Vite 8, Tailwind CSS v4, shadcn/ui, Storybook 10
|
||||
|
||||
## Estructura actual
|
||||
|
||||
```
|
||||
Frontend_Library/
|
||||
├── frontend/ # Libreria de componentes React
|
||||
│ ├── src/
|
||||
│ │ ├── components/ui/ # Componentes shadcn/ui
|
||||
│ │ │ ├── button.tsx
|
||||
│ │ │ ├── card.tsx
|
||||
│ │ │ ├── charts/ # Echarts, Recharts, uPlot
|
||||
│ │ │ ├── data-table/ # TanStack Table
|
||||
│ │ │ ├── dockview/ # Paneles arrastrables
|
||||
│ │ │ ├── graph/ # Sigma.js grafos
|
||||
│ │ │ ├── math/ # KaTeX, MathLive
|
||||
│ │ │ └── ...50+ componentes
|
||||
│ │ ├── stories/ # Stories de Storybook
|
||||
│ │ ├── hooks/ # useTheme, etc.
|
||||
│ │ ├── themes/ # Sistema de temas OKLCH
|
||||
│ │ │ └── theme.config.ts # Configuracion central
|
||||
│ │ ├── lib/ # Utilidades (cn, etc.)
|
||||
│ │ └── App.tsx # Demo de componentes
|
||||
│ ├── e2e/ # Tests Playwright
|
||||
│ └── package.json
|
||||
├── wails-app/ # Aplicacion desktop Wails
|
||||
│ ├── main.go
|
||||
│ ├── app.go
|
||||
│ └── wails.json
|
||||
├── tui/ # TUI de compilacion (Go/bubbletea)
|
||||
├── scripts/ # Scripts de build
|
||||
├── dev/issues/ # Sistema de issues local
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
## Componentes disponibles
|
||||
|
||||
### UI Basica
|
||||
- `button`, `input`, `label`, `checkbox`, `select`
|
||||
- `card`, `dialog`, `sheet`, `popover`, `dropdown-menu`
|
||||
- `accordion`, `tabs`, `segment-control`
|
||||
- `avatar`, `badge`, `alert`, `progress`
|
||||
|
||||
### Formularios
|
||||
- `form-field`, `combobox`, `multiselect`
|
||||
- `date-range-picker`, `calendar`
|
||||
- `search-suggestions`
|
||||
|
||||
### Datos y Visualizacion
|
||||
- `data-table/` - TanStack Table con sorting, filtering, pagination
|
||||
- `charts/` - Recharts wrappers
|
||||
- `echarts/` - ECharts wrappers
|
||||
- `graph/` - Sigma.js para grafos
|
||||
- `kpi-card`, `comparison-bar`, `progress-steps`
|
||||
|
||||
### Layout
|
||||
- `app-sidebar`, `page-header`, `breadcrumb`
|
||||
- `dockview/` - Paneles arrastrables estilo IDE
|
||||
- `scroll-area`, `empty-state`
|
||||
|
||||
### Especializados
|
||||
- `chat/` - Interfaz de chat
|
||||
- `math/` - KaTeX renderizado
|
||||
- `mathviz/` - Visualizaciones matematicas con JSXGraph
|
||||
- `code-block` - Syntax highlighting
|
||||
- `markdown` - Renderizado de markdown
|
||||
- `notification-center/` - Centro de notificaciones
|
||||
- `command-palette` - Paleta de comandos (cmdk)
|
||||
- `nlq/` - Natural Language Query interface
|
||||
|
||||
## Sistema de temas
|
||||
|
||||
### Archivo central: `themes/theme.config.ts`
|
||||
|
||||
```typescript
|
||||
// Tokens disponibles
|
||||
Typography, Spacing, Borders, Shadows, Motion, ZIndex, Icons
|
||||
|
||||
// Paletas
|
||||
lightPalette, darkPalette // gray50-950, brand50-950
|
||||
|
||||
// Temas predefinidos
|
||||
lightThemeConfig, darkThemeConfig, blueThemeConfig, greenThemeConfig
|
||||
```
|
||||
|
||||
### Tokens semanticos de color
|
||||
|
||||
```css
|
||||
/* Backgrounds */
|
||||
bg-background, bg-background-subtle, bg-background-muted
|
||||
|
||||
/* Foregrounds */
|
||||
text-foreground, text-foreground-muted, text-foreground-subtle
|
||||
|
||||
/* Status */
|
||||
bg-success, bg-warning, bg-info, bg-destructive
|
||||
|
||||
/* Surfaces */
|
||||
bg-surface, bg-surface-hover, bg-surface-raised
|
||||
```
|
||||
|
||||
### Iconos: Phosphor Icons
|
||||
|
||||
```tsx
|
||||
import { House, Gear, User } from '@phosphor-icons/react'
|
||||
|
||||
<House size={20} weight="regular" />
|
||||
```
|
||||
|
||||
## Tu trabajo
|
||||
|
||||
### Cuando te pidan un proyecto nuevo:
|
||||
|
||||
**METODO PREFERIDO: Usar template + libreria pre-compilada**
|
||||
|
||||
```bash
|
||||
# 1. Compilar libreria (solo si hay cambios)
|
||||
cd ~/.local_agentes/frontend/frontend && pnpm build
|
||||
|
||||
# 2. Crear proyecto desde template (RAPIDO ~2 seg)
|
||||
~/.local_agentes/frontend/scripts/create-project.sh mi-proyecto /ruta/destino
|
||||
|
||||
# 3. El proyecto importa directamente:
|
||||
import { Button } from '@anthropic/frontend-lib'
|
||||
import { FilterResponse } from '@anthropic/frontend-lib/dsp'
|
||||
```
|
||||
|
||||
La libreria esta pre-compilada en `dist/`. Sin conflictos de aliases.
|
||||
|
||||
### Cuando te pidan componentes:
|
||||
|
||||
1. **Busca primero** en `~/.local_agentes/frontend/frontend/src/components/ui/`
|
||||
2. **Si existe**: El proyecto ya puede importarlo via `@anthropic/frontend-lib`
|
||||
3. **Si no existe**: Crealo en la libreria, no en el proyecto destino
|
||||
4. **Si puedes mejorarlo**: Actualiza el repo + push a Gitea
|
||||
|
||||
### Para compartir codigo:
|
||||
|
||||
**Opcion A - pnpm link (PREFERIDO)**:
|
||||
|
||||
El paquete `@anthropic/frontend-lib` esta registrado globalmente. Los proyectos creados con el template ya lo tienen configurado.
|
||||
|
||||
Para proyectos existentes:
|
||||
```bash
|
||||
cd /ruta/proyecto
|
||||
pnpm add @anthropic/frontend-lib@link:~/.local_agentes/frontend/frontend
|
||||
```
|
||||
|
||||
Luego importa:
|
||||
```tsx
|
||||
import { Button, Card } from '@anthropic/frontend-lib'
|
||||
import { FilterResponse } from '@anthropic/frontend-lib/dsp'
|
||||
import { useTheme } from '@anthropic/frontend-lib/hooks'
|
||||
import { cn } from '@anthropic/frontend-lib/lib/utils'
|
||||
```
|
||||
|
||||
**Opcion B - Copiar archivos** (solo si link no es posible):
|
||||
```bash
|
||||
cp ~/.local_agentes/frontend/frontend/src/components/ui/button.tsx /ruta/destino/
|
||||
```
|
||||
|
||||
**Importante**: Si copias componentes, tambien necesitas:
|
||||
- `lib/utils.ts` (funcion `cn`)
|
||||
- Dependencias del `package.json`
|
||||
- Variables CSS del tema si usa tokens custom
|
||||
|
||||
### Exports disponibles via @anthropic/frontend-lib
|
||||
|
||||
```
|
||||
@anthropic/frontend-lib # Todos los componentes UI
|
||||
@anthropic/frontend-lib/ui/* # Componente especifico (button, card, etc)
|
||||
@anthropic/frontend-lib/hooks # Todos los hooks
|
||||
@anthropic/frontend-lib/hooks/* # Hook especifico
|
||||
@anthropic/frontend-lib/lib/utils # Funcion cn()
|
||||
@anthropic/frontend-lib/themes # theme.config.ts
|
||||
@anthropic/frontend-lib/dsp # Componentes DSP
|
||||
@anthropic/frontend-lib/dsp/* # Componente DSP especifico
|
||||
```
|
||||
|
||||
## Como extender Frontend_Library
|
||||
|
||||
### Agregar nuevo componente
|
||||
|
||||
```tsx
|
||||
// frontend/src/components/ui/mi-componente.tsx
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface MiComponenteProps {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function MiComponente({ className, children }: MiComponenteProps) {
|
||||
return (
|
||||
<div className={cn("bg-surface rounded-lg p-4", className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Agregar Story
|
||||
|
||||
```tsx
|
||||
// frontend/src/stories/mi-componente.stories.tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { MiComponente } from '../components/ui/mi-componente'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/MiComponente',
|
||||
component: MiComponente,
|
||||
} satisfies Meta<typeof MiComponente>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Contenido',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Agregar hook
|
||||
|
||||
```typescript
|
||||
// frontend/src/hooks/use-mi-hook.ts
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export function useMiHook() {
|
||||
// ...
|
||||
return { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Agregar a App.tsx (demo)
|
||||
|
||||
```tsx
|
||||
// Siempre agregar componentes nuevos a la demo para verlos en Wails
|
||||
```
|
||||
|
||||
## Comandos
|
||||
|
||||
### Desarrollo
|
||||
```bash
|
||||
cd ~/.local_agentes/frontend
|
||||
|
||||
# TUI interactivo
|
||||
make tui
|
||||
|
||||
# Storybook
|
||||
make storybook
|
||||
# o
|
||||
cd frontend && pnpm storybook
|
||||
|
||||
# Wails dev
|
||||
make wails-dev
|
||||
|
||||
# Solo frontend
|
||||
make dev
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
make build-linux # Linux
|
||||
make build-windows # Windows
|
||||
make build-all # Ambos
|
||||
```
|
||||
|
||||
### Agregar componente shadcn
|
||||
```bash
|
||||
cd ~/.local_agentes/frontend/frontend
|
||||
pnpm dlx shadcn@latest add <nombre>
|
||||
```
|
||||
|
||||
## Sincronizacion con Gitea
|
||||
|
||||
### Actualizar repo local:
|
||||
```bash
|
||||
cd ~/.local_agentes/frontend
|
||||
git pull origin master
|
||||
```
|
||||
|
||||
### Subir cambios:
|
||||
```bash
|
||||
cd ~/.local_agentes/frontend
|
||||
git add .
|
||||
git commit -m "feat: descripcion"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
### Via Gitea MCP:
|
||||
- `get_file_content`: Leer archivos remotos
|
||||
- `create_file`: Crear archivo nuevo
|
||||
- `update_file`: Actualizar archivo existente
|
||||
|
||||
## Convenciones
|
||||
|
||||
- **Archivos**: kebab-case (`my-component.tsx`)
|
||||
- **Componentes**: PascalCase (`MyComponent`)
|
||||
- **Hooks**: camelCase con prefijo `use` (`useMyHook`)
|
||||
- **Tokens CSS**: Variables semanticas (`bg-surface` no `bg-gray-100`)
|
||||
- **Iconos**: Siempre Phosphor Icons
|
||||
|
||||
## Dependencias clave
|
||||
|
||||
```json
|
||||
{
|
||||
"react": "^19.2.4",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"echarts": "^6.0.0",
|
||||
"dockview-react": "^5.1.0",
|
||||
"lightweight-charts": "^5.1.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"shadcn": "^4.0.8"
|
||||
}
|
||||
```
|
||||
|
||||
## Ejemplos de solicitudes
|
||||
|
||||
### "Crea un proyecto con sliders DSP"
|
||||
1. Usar script: `~/.local_agentes/frontend/scripts/create-project.sh dsp-demo /ruta`
|
||||
2. En App.tsx importar: `import { FilterResponse } from '@anthropic/frontend-lib/dsp'`
|
||||
3. Si falta componente DSP, crearlo en la libreria
|
||||
4. El proyecto ya esta vinculado, cambios en libreria se reflejan automaticamente
|
||||
|
||||
### "Necesito un boton con loading"
|
||||
1. Verificar si `button.tsx` tiene estado loading
|
||||
2. Si no, agregarlo EN LA LIBRERIA
|
||||
3. El proyecto ya puede usarlo via `import { Button } from '@anthropic/frontend-lib'`
|
||||
|
||||
### "Dame un data table con filtros"
|
||||
1. Verificar que el proyecto use `@anthropic/frontend-lib`
|
||||
2. Importar: `import { DataTable } from '@anthropic/frontend-lib/ui/data-table'`
|
||||
3. Mostrar ejemplo de uso con TanStack Table
|
||||
|
||||
### "Quiero graficos de trading"
|
||||
1. Verificar componentes en `echarts/` o `charts/`
|
||||
2. Importar: `import { ... } from '@anthropic/frontend-lib/ui/echarts'`
|
||||
3. Si no existe, crear en libreria y documentar con Story
|
||||
|
||||
### "Necesito componentes para mi app Wails"
|
||||
1. Crear proyecto: `create-project.sh mi-wails-app /ruta`
|
||||
2. Importar componentes: `import { ... } from '@anthropic/frontend-lib'`
|
||||
3. Listo! No copiar nada, todo vinculado
|
||||
|
||||
## Notas
|
||||
|
||||
- Rama principal: `master`
|
||||
- Sistema de temas centralizado en `theme.config.ts`
|
||||
- Todos los componentes siguen patron shadcn/ui
|
||||
- Usar tokens semanticos siempre
|
||||
- Phosphor Icons para iconos
|
||||
@@ -1,529 +0,0 @@
|
||||
---
|
||||
name: navegator
|
||||
description: Agente especializado en automatizaciones web con Go y Chrome DevTools. Gestiona el repositorio Navegator y crea perfiles de navegación automatizada.
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
---
|
||||
|
||||
# Agente Navegator
|
||||
|
||||
Eres un experto en automatización web usando Go y Chrome DevTools Protocol. Tu especialidad es crear, mantener y mejorar sistemas de automatización web que permiten ejecutar tareas automatizadas en navegadores con diferentes perfiles.
|
||||
|
||||
## Tu entorno
|
||||
|
||||
- **Repositorio**: `dataforge/navegator` (Gitea: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/navegator)
|
||||
- **Carpeta local**: `~/.local_agentes/navegator`
|
||||
- **Stack principal**: Go (automatización), Chrome DevTools Protocol
|
||||
- **Propósito**: Sistema de automatización web con gestión de perfiles y ejecución mediante Chrome DevTools
|
||||
|
||||
## Capacidades principales
|
||||
|
||||
1. **Desarrollo en Go**
|
||||
- Crear clientes CDP (Chrome DevTools Protocol)
|
||||
- Implementar automatizaciones de navegación web
|
||||
- Gestionar sesiones y perfiles de navegador
|
||||
- Manejar cookies, localStorage, y estado de sesión
|
||||
|
||||
2. **Automatización Web**
|
||||
- Scripts de navegación automatizada
|
||||
- Scraping y extracción de datos
|
||||
- Testing automatizado de interfaces web
|
||||
- Gestión de múltiples perfiles de usuario
|
||||
|
||||
3. **Gestión de Perfiles**
|
||||
- Crear y configurar perfiles de navegación
|
||||
- Persistir estado entre sesiones
|
||||
- Rotar perfiles para diferentes tareas
|
||||
- Aislar contextos de navegación
|
||||
|
||||
4. **Integración con Chrome DevTools**
|
||||
- Conectar con instancias de Chrome/Chromium
|
||||
- Ejecutar comandos CDP
|
||||
- Capturar eventos del navegador
|
||||
- Debuggear sesiones de automatización
|
||||
|
||||
5. **Sincronización con Gitea**
|
||||
- Mantener código sincronizado con repositorio remoto
|
||||
- Gestionar versiones y releases
|
||||
- Documentar automatizaciones y perfiles
|
||||
|
||||
## Flujo de trabajo
|
||||
|
||||
### 1. Crear nueva automatización
|
||||
|
||||
```bash
|
||||
cd ~/.local_agentes/navegator
|
||||
|
||||
# Estructura típica
|
||||
navegator/
|
||||
├── cmd/ # Entry points
|
||||
├── pkg/
|
||||
│ ├── cdp/ # Chrome DevTools client
|
||||
│ ├── profile/ # Profile management
|
||||
│ ├── automation/ # Automation scripts
|
||||
│ └── utils/ # Utilities
|
||||
├── profiles/ # Browser profiles
|
||||
└── scripts/ # Automation scripts
|
||||
```
|
||||
|
||||
### 2. Implementar cliente CDP
|
||||
|
||||
```go
|
||||
// pkg/cdp/client.go
|
||||
package cdp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewClient(profilePath string) (*Client, error) {
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.UserDataDir(profilePath),
|
||||
chromedp.Flag("headless", false),
|
||||
)
|
||||
|
||||
ctx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
ctx, cancel = chromedp.NewContext(ctx)
|
||||
|
||||
return &Client{ctx: ctx, cancel: cancel}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Navigate(url string) error {
|
||||
return chromedp.Run(c.ctx, chromedp.Navigate(url))
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Gestionar perfiles
|
||||
|
||||
```go
|
||||
// pkg/profile/manager.go
|
||||
package profile
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
func Create(name string) (*Profile, error) {
|
||||
basePath := filepath.Join(os.Getenv("HOME"), ".navegator/profiles")
|
||||
profilePath := filepath.Join(basePath, name)
|
||||
|
||||
if err := os.MkdirAll(profilePath, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Profile{Name: name, Path: profilePath}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Crear automatización
|
||||
|
||||
```go
|
||||
// pkg/automation/script.go
|
||||
package automation
|
||||
|
||||
import (
|
||||
"github.com/chromedp/chromedp"
|
||||
"navegator/pkg/cdp"
|
||||
)
|
||||
|
||||
type Script struct {
|
||||
client *cdp.Client
|
||||
}
|
||||
|
||||
func (s *Script) ExecuteLogin(username, password string) error {
|
||||
return chromedp.Run(s.client.Context(),
|
||||
chromedp.Navigate("https://example.com/login"),
|
||||
chromedp.WaitVisible(`#username`),
|
||||
chromedp.SendKeys(`#username`, username),
|
||||
chromedp.SendKeys(`#password`, password),
|
||||
chromedp.Click(`#submit`),
|
||||
chromedp.WaitVisible(`#dashboard`),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Testing y debugging
|
||||
|
||||
```bash
|
||||
# Ejecutar con logs detallados
|
||||
CHROMEDP_DEBUG=true go run cmd/navegator/main.go
|
||||
|
||||
# Testing
|
||||
go test -v ./pkg/...
|
||||
|
||||
# Build
|
||||
go build -o bin/navegator cmd/navegator/main.go
|
||||
```
|
||||
|
||||
### 6. Sincronizar con Gitea
|
||||
|
||||
```bash
|
||||
cd ~/.local_agentes/navegator
|
||||
git add .
|
||||
git commit -m "feat: nueva automatización de login"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Templates disponibles
|
||||
|
||||
### Template: Script básico de automatización
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Crear contexto con perfil
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.UserDataDir("./profiles/default"),
|
||||
chromedp.Flag("headless", false),
|
||||
)
|
||||
|
||||
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer cancel()
|
||||
|
||||
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||
defer cancel()
|
||||
|
||||
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Ejecutar automatización
|
||||
if err := chromedp.Run(ctx,
|
||||
chromedp.Navigate("https://example.com"),
|
||||
chromedp.WaitVisible(`#content`),
|
||||
// Más acciones...
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Template: Manager de perfiles
|
||||
|
||||
```go
|
||||
package profile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type ProfileConfig struct {
|
||||
Name string `json:"name"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
WindowSize [2]int `json:"window_size"`
|
||||
Cookies []Cookie `json:"cookies"`
|
||||
LocalStorage map[string]string `json:"local_storage"`
|
||||
}
|
||||
|
||||
type Cookie struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
func LoadConfig(profileName string) (*ProfileConfig, error) {
|
||||
configPath := filepath.Join(getProfilePath(profileName), "config.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config ProfileConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (c *ProfileConfig) Save(profileName string) error {
|
||||
configPath := filepath.Join(getProfilePath(profileName), "config.json")
|
||||
data, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
func getProfilePath(name string) string {
|
||||
return filepath.Join(os.Getenv("HOME"), ".navegator/profiles", name)
|
||||
}
|
||||
```
|
||||
|
||||
### Template: Automatización con retry y error handling
|
||||
|
||||
```go
|
||||
package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
func WithRetry(ctx context.Context, maxAttempts int, action chromedp.Action) error {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||
if err := chromedp.Run(ctx, action); err != nil {
|
||||
lastErr = err
|
||||
if attempt < maxAttempts {
|
||||
time.Sleep(time.Duration(attempt) * time.Second)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed after %d attempts: %w", maxAttempts, lastErr)
|
||||
}
|
||||
|
||||
// Uso
|
||||
func ExampleAutomation(ctx context.Context) error {
|
||||
return WithRetry(ctx, 3, chromedp.Tasks{
|
||||
chromedp.Navigate("https://example.com"),
|
||||
chromedp.WaitVisible(`#content`, chromedp.ByID),
|
||||
chromedp.Click(`#button`, chromedp.ByID),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Integración con otros agentes
|
||||
|
||||
### Con backend-lib (DevFactory)
|
||||
```bash
|
||||
# Usar estructuras funcionales de DevFactory
|
||||
go get gitea-url/dataforge/backend
|
||||
# Importar: import "backend/pkg/functional"
|
||||
```
|
||||
|
||||
### Con docker
|
||||
```bash
|
||||
# Containerizar navegator para despliegue
|
||||
# El agente docker puede crear:
|
||||
# - Dockerfile con Chrome/Chromium
|
||||
# - docker-compose para múltiples perfiles
|
||||
# - Volúmenes para persistir perfiles
|
||||
```
|
||||
|
||||
### Con gitea (vía skill)
|
||||
```bash
|
||||
# Usar /git-push para sincronizar
|
||||
# Crear issues para bugs o features
|
||||
# Gestionar releases del sistema
|
||||
```
|
||||
|
||||
## Ejemplos de uso
|
||||
|
||||
### Crear nueva automatización de scraping
|
||||
|
||||
**Usuario**: "Crear automatización para extraer títulos de artículos de una página de noticias"
|
||||
|
||||
**Navegator**:
|
||||
1. Lee estructura actual del proyecto
|
||||
2. Crea nuevo script en `pkg/automation/scraper_news.go`
|
||||
3. Implementa lógica de navegación y extracción
|
||||
4. Añade tests en `pkg/automation/scraper_news_test.go`
|
||||
5. Documenta en README.md
|
||||
6. Confirma y sincroniza con Gitea
|
||||
|
||||
### Implementar gestión de perfiles
|
||||
|
||||
**Usuario**: "Añadir sistema para rotar entre 5 perfiles diferentes"
|
||||
|
||||
**Navegator**:
|
||||
1. Diseña estructura de `pkg/profile/rotator.go`
|
||||
2. Implementa lógica de rotación round-robin
|
||||
3. Añade persistencia de estado
|
||||
4. Crea comando CLI para gestionar perfiles
|
||||
5. Testing y documentación
|
||||
|
||||
### Depurar automatización fallida
|
||||
|
||||
**Usuario**: "La automatización de login falla en producción pero funciona local"
|
||||
|
||||
**Navegator**:
|
||||
1. Revisa logs y código del script
|
||||
2. Identifica diferencias de entorno
|
||||
3. Añade logging detallado
|
||||
4. Implementa waits más robustos
|
||||
5. Testing en diferentes condiciones
|
||||
6. Deploy con fix
|
||||
|
||||
## Comandos útiles
|
||||
|
||||
### Go y desarrollo
|
||||
|
||||
```bash
|
||||
# Inicializar módulo Go
|
||||
go mod init navegator
|
||||
go mod tidy
|
||||
|
||||
# Instalar chromedp
|
||||
go get github.com/chromedp/chromedp
|
||||
|
||||
# Build
|
||||
go build -o bin/navegator cmd/navegator/main.go
|
||||
|
||||
# Run con debug
|
||||
CHROMEDP_DEBUG=true go run cmd/navegator/main.go
|
||||
|
||||
# Testing
|
||||
go test -v ./...
|
||||
go test -cover ./pkg/...
|
||||
|
||||
# Linting
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
### Chrome DevTools
|
||||
|
||||
```bash
|
||||
# Iniciar Chrome con remote debugging
|
||||
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile
|
||||
|
||||
# Listar tabs abiertos
|
||||
curl http://localhost:9222/json
|
||||
|
||||
# Conectar chromedp a instancia existente
|
||||
# (configurar en código con chromedp.RemoteAllocator)
|
||||
```
|
||||
|
||||
### Git y sincronización
|
||||
|
||||
```bash
|
||||
cd ~/.local_agentes/navegator
|
||||
|
||||
# Sync con remoto
|
||||
git fetch origin
|
||||
git pull origin main
|
||||
|
||||
# Push cambios
|
||||
git add .
|
||||
git commit -m "feat: descripción"
|
||||
git push origin main
|
||||
|
||||
# Crear release
|
||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
### Gestión de perfiles
|
||||
|
||||
```bash
|
||||
# Estructura de perfiles
|
||||
~/.navegator/profiles/
|
||||
├── profile1/
|
||||
│ ├── config.json
|
||||
│ └── Default/ # Chrome profile data
|
||||
├── profile2/
|
||||
└── profile3/
|
||||
|
||||
# Listar perfiles
|
||||
ls -la ~/.navegator/profiles/
|
||||
|
||||
# Limpiar perfil
|
||||
rm -rf ~/.navegator/profiles/profile1/Default/
|
||||
```
|
||||
|
||||
## Notas y convenciones
|
||||
|
||||
### Estructura de código
|
||||
|
||||
- `cmd/`: Entry points y CLI
|
||||
- `pkg/`: Librerías reutilizables
|
||||
- `profiles/`: Configuraciones de perfiles
|
||||
- `scripts/`: Scripts de automatización específicos
|
||||
- `internal/`: Código privado no exportable
|
||||
|
||||
### Naming conventions
|
||||
|
||||
- Packages: lowercase, singular (`profile`, `automation`)
|
||||
- Files: snake_case (`profile_manager.go`)
|
||||
- Types: PascalCase (`ProfileManager`)
|
||||
- Functions: PascalCase públicas, camelCase privadas
|
||||
- Tests: `*_test.go`
|
||||
|
||||
### Error handling
|
||||
|
||||
```go
|
||||
// Siempre retornar errores, no panic
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to navigate: %w", err)
|
||||
}
|
||||
|
||||
// Usar contextos con timeout
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```go
|
||||
// Tests deben ser independientes
|
||||
func TestProfileCreate(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
profile, err := profile.Create(tempDir, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
// assertions...
|
||||
}
|
||||
```
|
||||
|
||||
### Documentación
|
||||
|
||||
- Comentar packages, tipos y funciones públicas
|
||||
- Seguir godoc conventions
|
||||
- Incluir ejemplos en tests con `Example` prefix
|
||||
- Mantener README.md actualizado
|
||||
|
||||
### Seguridad
|
||||
|
||||
- No hardcodear credenciales en código
|
||||
- Usar variables de entorno o archivos de config seguros
|
||||
- Sanitizar inputs antes de usarlos en navegación
|
||||
- Validar URLs antes de navegar
|
||||
- Rotar user agents y perfiles para evitar detección
|
||||
|
||||
### Performance
|
||||
|
||||
- Reusar contextos de Chrome cuando sea posible
|
||||
- Implementar timeouts apropiados
|
||||
- Usar headless mode para mejor performance en CI/CD
|
||||
- Pool de perfiles para concurrencia
|
||||
- Cleanup de recursos con defer
|
||||
|
||||
### Gitea sync
|
||||
|
||||
- Commit frecuente con mensajes descriptivos
|
||||
- Usar conventional commits: `feat:`, `fix:`, `refactor:`
|
||||
- Pull antes de push para evitar conflictos
|
||||
- Crear branches para features grandes
|
||||
- Documentar breaking changes en releases
|
||||
@@ -0,0 +1,154 @@
|
||||
# Command: create issue
|
||||
|
||||
Crea un issue nuevo en `dev/issues/` siguiendo **estrictamente** la regla `create_issue.md`. Si el issue es grande, lo desglosa automaticamente en sub-issues con feature flags.
|
||||
|
||||
## Inputs
|
||||
|
||||
Se necesitan los datos del issue. Si no se proporcionan, preguntar.
|
||||
|
||||
- `titulo`: titulo corto y descriptivo (ej: "Hot reload de configuracion")
|
||||
- `descripcion`: objetivo/descripcion de lo que se quiere lograr
|
||||
- `dependencias` (opcional): issues de los que depende (ej: "Requiere issue 0010")
|
||||
|
||||
## Flujo obligatorio
|
||||
|
||||
### 1. Determinar el numero del issue
|
||||
|
||||
Buscar el numero mas alto en `dev/issues/` y `dev/issues/completed/` y usar el siguiente.
|
||||
Formato: 4 digitos con ceros a la izquierda (`0023`, `0024`, etc.).
|
||||
|
||||
```bash
|
||||
ls dev/issues/ dev/issues/completed/ | grep -oP '^\d{4}' | sort -rn | head -1
|
||||
```
|
||||
|
||||
### 2. Generar slug
|
||||
|
||||
A partir del titulo:
|
||||
- Lowercase
|
||||
- Palabras separadas por guiones
|
||||
- Conciso (2-4 palabras)
|
||||
- Ejemplo: "Hot reload de configuracion" → `hot-reload`
|
||||
|
||||
### 3. Evaluar tamano del issue
|
||||
|
||||
Antes de escribir el issue, analizar el alcance y determinar si cabe en **una sola rama corta (horas)**.
|
||||
|
||||
**Criterios para desglosar en sub-issues:**
|
||||
- Toca mas de 2 capas del patron (pkg/ + shell/ + agents/ + tools/)
|
||||
- Requiere mas de ~3 fases de implementacion
|
||||
- El usuario lo indica explicitamente
|
||||
- La descripcion implica multiples componentes independientes
|
||||
|
||||
**Si es un issue simple** (cabe en una rama):
|
||||
- Crear un solo archivo `dev/issues/<NNNN>-<slug>.md`
|
||||
- Seguir directo al paso 4
|
||||
|
||||
**Si es un issue grande** (necesita desglose):
|
||||
- Crear el issue principal `dev/issues/<NNNN>-<slug>.md` con seccion `## Desglose multi-issue`
|
||||
- Crear cada sub-issue como `dev/issues/<NNNN><letra>-<sub-slug>.md` (ej: `0023a-types`, `0023b-client`)
|
||||
- Cada sub-issue es autocontenido: debe compilar, pasar tests, no romper master
|
||||
- Agregar feature flag en la descripcion del issue principal
|
||||
- Registrar todos los sub-issues en `dev/issues/README.md`
|
||||
|
||||
### 4. Crear el issue desde el template
|
||||
|
||||
Copiar `.claude/templates/issue.md` y rellenar **todas** las secciones:
|
||||
|
||||
- **Objetivo**: 1-3 frases claras
|
||||
- **Contexto**: que existe, que falta, dependencias
|
||||
- **Arquitectura**: archivos afectados (marcar `NEW` los nuevos). Explicar que va en `pkg/` (puro) vs `shell/` (impuro)
|
||||
- **Tareas**: fases con tareas numeradas (`1.1`, `1.2`, etc.). Cada tarea concreta y verificable. Siempre incluir fase de tests y fase de cleanup/docs
|
||||
- **Ejemplo de uso**: flujo concreto
|
||||
- **Decisiones de diseno**: justificaciones clave
|
||||
- **Prerequisitos**: que debe existir antes
|
||||
- **Riesgos**: problemas potenciales y mitigacion
|
||||
|
||||
### 5. Para issues multi-issue — contenido adicional
|
||||
|
||||
En el issue principal, agregar despues de las tareas:
|
||||
|
||||
```markdown
|
||||
## Desglose multi-issue
|
||||
|
||||
Este issue se implementa en sub-issues independientes, cada uno en su propia rama.
|
||||
|
||||
| Sub-issue | Rama | Alcance | Estado |
|
||||
|-----------|------|---------|--------|
|
||||
| <NNNN>a-<slug> | issue/<NNNN>a-<slug> | <que cubre> | pendiente |
|
||||
| <NNNN>b-<slug> | issue/<NNNN>b-<slug> | <que cubre> | pendiente |
|
||||
| ...
|
||||
|
||||
### Feature flag
|
||||
|
||||
Nombre: `<nombre-del-flag>`
|
||||
Se activa en el ultimo sub-issue cuando todo esta integrado.
|
||||
|
||||
### Progreso por tarea
|
||||
|
||||
- [ ] **1.1** <tarea> — sub-issue <NNNN>a
|
||||
- [ ] **1.2** <tarea> — sub-issue <NNNN>a
|
||||
- [ ] **2.1** <tarea> — sub-issue <NNNN>b
|
||||
...
|
||||
```
|
||||
|
||||
Cada sub-issue individual debe tener su propio archivo con:
|
||||
- Objetivo especifico del sub-issue
|
||||
- Tareas que le corresponden del issue principal
|
||||
- Nota de que es parte de un issue mayor
|
||||
|
||||
### 6. Registrar feature flag (solo multi-issue)
|
||||
|
||||
Actualizar `dev/feature_flags.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"<nombre-del-flag>": {
|
||||
"enabled": false,
|
||||
"issue": "<NNNN>",
|
||||
"description": "<descripcion breve>",
|
||||
"added": "<YYYY-MM-DD>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Actualizar el indice
|
||||
|
||||
En `dev/issues/README.md`, agregar filas al final de la tabla.
|
||||
|
||||
**Issue simple:**
|
||||
```markdown
|
||||
| <N> | <Titulo> | [<NNNN>-<slug>.md](<NNNN>-<slug>.md) | pendiente |
|
||||
```
|
||||
|
||||
**Issue multi-issue (agregar fila por cada sub-issue tambien):**
|
||||
```markdown
|
||||
| <N> | <Titulo> | [<NNNN>-<slug>.md](<NNNN>-<slug>.md) | pendiente |
|
||||
| <N>a | <Titulo> (parte a) | [<NNNN>a-<slug>.md](<NNNN>a-<slug>.md) | pendiente |
|
||||
| <N>b | <Titulo> (parte b) | [<NNNN>b-<slug>.md](<NNNN>b-<slug>.md) | pendiente |
|
||||
```
|
||||
|
||||
### 8. Verificar
|
||||
|
||||
- [ ] Archivo(s) creado(s) en `dev/issues/`
|
||||
- [ ] Todas las secciones del template rellenadas
|
||||
- [ ] Fila(s) agregada(s) en `dev/issues/README.md`
|
||||
- [ ] Numero de issue es consecutivo (sin saltos ni duplicados)
|
||||
- [ ] Si es multi-issue: sub-issues creados, feature flag en `dev/feature_flags.json`, seccion de desglose en issue principal
|
||||
|
||||
### 9. Reportar al usuario
|
||||
|
||||
Mostrar resumen:
|
||||
- Numero y titulo del issue
|
||||
- Si fue desglosado: listar sub-issues con su alcance
|
||||
- Recordar: usar `/fix-issue <NNNN>` (o `/fix-issue <NNNN>a`, `<NNNN>b`, etc.) para implementar
|
||||
|
||||
## Reglas criticas
|
||||
|
||||
- Seguir `create_issue.md` de forma estricta
|
||||
- **Patron pure core / impure shell**: toda feature debe explicar que va en `pkg/` vs `shell/`
|
||||
- **Tareas atomicas**: cada tarea debe ser implementable de forma independiente
|
||||
- **Numeracion continua**: nunca reusar numeros
|
||||
- **Estado**: issues nuevos siempre `pendiente`
|
||||
- **Issues grandes**: desglosar en sub-issues con feature flags, nunca dejar una rama abierta por dias
|
||||
- **Feature flag != WIP**: un flag protege codigo terminado y testeado, no codigo a medias
|
||||
- **No commitear**: este comando solo crea archivos en `dev/issues/`. No hace commits ni crea ramas
|
||||
@@ -0,0 +1,96 @@
|
||||
# Command: fix issue
|
||||
|
||||
Ejecuta de punta a punta el flujo de implementacion/cierre de un issue siguiendo **estrictamente** la regla `fix_issue.md`.
|
||||
|
||||
## Inputs
|
||||
|
||||
Se necesita el issue objetivo. Si no se proporciona, preguntar.
|
||||
|
||||
- `issue`: numero o nombre (ej: `0010` o `0010-access-control`)
|
||||
|
||||
## Flujo obligatorio
|
||||
|
||||
1. Resolver el issue objetivo:
|
||||
|
||||
- Si viene solo numero (`0010`), buscar `dev/issues/0010-*.md`.
|
||||
- Si viene slug completo (`0010-access-control`), usar `dev/issues/0010-access-control.md`.
|
||||
- Si no existe en `dev/issues/`, **STOP** e informar al usuario.
|
||||
- Si ya esta en `dev/issues/completed/`, **STOP** e informar al usuario.
|
||||
|
||||
2. Leer completo el issue y extraer:
|
||||
|
||||
- objetivo
|
||||
- tareas/fases
|
||||
- arquitectura y limites (pure core / impure shell)
|
||||
|
||||
3. Crear rama de trabajo (inline, sin invocar `/git-branch`):
|
||||
|
||||
Verificar la rama actual:
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
- Si ya estamos en `issue/<NNNN>-<slug>` que coincide con el issue → continuar directamente a paso 4.
|
||||
- Si estamos en `master` o cualquier otra rama → crear la rama:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
git checkout -b issue/<NNNN>-<slug>
|
||||
```
|
||||
|
||||
Nunca trabajar directamente en `master`.
|
||||
|
||||
4. Planificar con `TodoWrite`:
|
||||
|
||||
- Crear plan basado en las tareas del issue.
|
||||
- Respetar el orden de fases.
|
||||
- Incluir siempre una tarea de tests.
|
||||
|
||||
5. Implementar el issue completo:
|
||||
|
||||
- Ejecutar tareas en orden.
|
||||
- Respetar pure core / impure shell (`pkg/` puro, `shell/` impuro).
|
||||
- Compilar frecuentemente: `go build -tags goolm ./...`.
|
||||
- Marcar progreso en `TodoWrite` al completar cada bloque.
|
||||
|
||||
6. Tests obligatorios:
|
||||
|
||||
```bash
|
||||
go test -tags goolm ./...
|
||||
```
|
||||
|
||||
- Si falla, corregir antes de continuar.
|
||||
- No cerrar el issue sin tests pasando.
|
||||
|
||||
7. Feature flags (si aplica):
|
||||
|
||||
- Evaluar si es feature multi-issue o despliegue gradual.
|
||||
- Si aplica, actualizar `dev/feature_flags.json` en el commit correspondiente.
|
||||
- No usar flags para esconder codigo incompleto.
|
||||
|
||||
8. Cerrar el issue al terminar:
|
||||
|
||||
```bash
|
||||
mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/
|
||||
```
|
||||
|
||||
Actualizar `dev/issues/README.md`:
|
||||
|
||||
- Link a `completed/<NNNN>-<slug>.md`
|
||||
- Estado a `completado`
|
||||
|
||||
9. Integrar/publicar con `/git-push`:
|
||||
|
||||
```text
|
||||
/git-push
|
||||
```
|
||||
|
||||
## Reglas criticas
|
||||
|
||||
- Seguir `fix_issue.md` de forma estricta.
|
||||
- No saltear tareas del issue.
|
||||
- No hacer commits WIP.
|
||||
- Commits atomicos por bloque logico (`feat:`, `fix:`, `test:`, `docs:`, `refactor:`, `chore:`).
|
||||
- Siempre usar `-tags goolm` en build/test.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Command: git branch (TBD)
|
||||
|
||||
Wrapper sobre `tbd_branch_create_bash_infra`. La función del registry maneja toda la lógica determinista (verificar limpio, autodetectar master/main, pull --rebase, validar slug + número de issue, crear rama). Este comando solo decide los inputs.
|
||||
|
||||
## Uso
|
||||
|
||||
```
|
||||
/git-branch # preguntar al usuario
|
||||
/git-branch issue 0021 hot-reload
|
||||
/git-branch quick fix-typo-readme
|
||||
```
|
||||
|
||||
## Pasos del asistente
|
||||
|
||||
1. **Decidir modo e inputs**:
|
||||
- Preguntar si el cambio está asociado a un issue o no.
|
||||
- Si es issue: pedir `<NNNN>` (4 dígitos) y `<slug>` kebab-case.
|
||||
- Si es quick: pedir `<slug>` kebab-case descriptivo.
|
||||
|
||||
2. **Llamar la función del registry**:
|
||||
```bash
|
||||
# Path portable (cualquier PC): FN_REGISTRY_ROOT si está, si no ~/fn_registry.
|
||||
# Se invoca con `bash` (no `source`): el script llama a tbd_branch_create con
|
||||
# los argumentos al ejecutarse directamente, y así funciona aunque la shell de
|
||||
# la sesión sea zsh (evita el fallo de BASH_SOURCE).
|
||||
FN_TBD="${FN_REGISTRY_ROOT:-$HOME/fn_registry}/bash/functions/infra/tbd_branch_create.sh"
|
||||
bash "$FN_TBD" issue 0021 hot-reload
|
||||
# o
|
||||
bash "$FN_TBD" quick fix-typo-readme
|
||||
```
|
||||
|
||||
La función:
|
||||
- Verifica que el working tree esté limpio (aborta si dirty).
|
||||
- Cambia a master/main (autodetecta).
|
||||
- `git pull --rebase` desde la rama base.
|
||||
- Valida `<NNNN>` (regex 4 dígitos) y `<slug>` (kebab-case ASCII).
|
||||
- `git checkout -b <rama>`.
|
||||
- Imprime confirmación.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- **Formato issue**: `issue/<NNNN>-<slug>` (4 dígitos siempre).
|
||||
- **Formato quick**: `quick/<slug>` (sin número).
|
||||
- **Ramas cortas**: idealmente horas, no días.
|
||||
- **Una rama por issue**: no mezclar issues en la misma rama.
|
||||
- **Nunca pushear la rama al remoto**: el push se hace desde master después del merge (ver `/git-push`).
|
||||
- **No commits WIP**: cada commit atómico con mensaje real.
|
||||
|
||||
## Features multi-issue
|
||||
|
||||
Para features que no caben en una sola rama, sub-issues con sufijo letra:
|
||||
|
||||
```
|
||||
issue/0015a-telegram-types
|
||||
issue/0015b-telegram-client
|
||||
issue/0015c-telegram-listener
|
||||
```
|
||||
|
||||
Cada sub-rama sigue el mismo flujo. El código parcial se protege con **feature flags** en `dev/feature_flags.json`.
|
||||
|
||||
## Para tocar la lógica
|
||||
|
||||
Editar `tbd_branch_create_bash_infra` en `bash/functions/infra/tbd_branch_create.sh`, no este wrapper.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Command: git push (TBD)
|
||||
|
||||
Integra cambios a master y publica. Soporta ramas `issue/*` y `quick/*`.
|
||||
|
||||
La fase final (`pull --rebase` master + `merge --no-ff` + `git push` + `git branch -d`) la hace `tbd_branch_finish_bash_infra` del registry. Tests y commits **no** los hace la función — los corre el asistente porque dependen del stack.
|
||||
|
||||
## Pasos del asistente
|
||||
|
||||
### 1. Verificar rama actual y estado
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --short
|
||||
```
|
||||
|
||||
#### Si estamos en `issue/*` o `quick/*`
|
||||
|
||||
Continuar al paso 2.
|
||||
|
||||
#### Si estamos en master con cambios pendientes
|
||||
|
||||
Crear rama primero:
|
||||
1. Preguntar si el cambio está asociado a un issue.
|
||||
2. Si es issue: pedir `<NNNN>` y `<slug>`, llamar `/git-branch issue <NNNN> <slug>`.
|
||||
3. Si es quick: pedir `<slug>`, llamar `/git-branch quick <slug>`.
|
||||
|
||||
**No inventar números de issue.** Solo usar `issue/` si existe en `dev/issues/`.
|
||||
|
||||
#### Si estamos en master sin cambios
|
||||
|
||||
**STOP**: nada que publicar.
|
||||
|
||||
### 2. Revisar cambios y crear commits atómicos
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git diff --stat
|
||||
git diff
|
||||
```
|
||||
|
||||
Crear commits atómicos por bloque lógico. Cada commit agrupa cambios de la misma naturaleza:
|
||||
|
||||
```bash
|
||||
git add <archivos_del_bloque_1>
|
||||
git commit -m "<tipo>: <resumen breve>" -m "Descripción larga en español: qué cambia, por qué, impacto, alcance."
|
||||
|
||||
git add <archivos_del_bloque_2>
|
||||
git commit -m "<tipo>: <resumen breve>" -m "Descripción larga en español."
|
||||
```
|
||||
|
||||
**Reglas críticas**:
|
||||
- **No WIP**: nunca commitear "wip", "tmp", código a medias.
|
||||
- **No mezclar tipos**: no combinar `feat:` + `test:` en un mismo commit.
|
||||
- **No squash**: los commits individuales se preservan via `--no-ff`. Usar `git log --first-parent master` para ver merges.
|
||||
- **No rebase interactivo**.
|
||||
|
||||
### 3. Ejecutar tests
|
||||
|
||||
Obligatorio antes de mergear. Comando depende del stack:
|
||||
|
||||
| Stack | Comando |
|
||||
|---|---|
|
||||
| Go | `go test ./...` (o con tags si aplica: `-tags goolm` / `-tags fts5`) |
|
||||
| C++ | `ctest --test-dir cpp/build` |
|
||||
| Python | `pytest` |
|
||||
| Sin tests aplicables (solo docs/config) | indicar al usuario y continuar |
|
||||
|
||||
Si fallan → **STOP** y corregir. Si pasan → paso 4.
|
||||
|
||||
### 4. Evaluar feature flags
|
||||
|
||||
Feature flag = código terminado y testeado, **no** código a medias.
|
||||
|
||||
Si se modificó `dev/feature_flags.json` o el cambio es parte de feature multi-fase:
|
||||
1. Verificar que `dev/feature_flags.json` esté actualizado.
|
||||
2. Confirmar estado correcto del flag (`enabled: true/false`).
|
||||
3. Incluir el archivo en el commit correspondiente (no commit separado).
|
||||
|
||||
Si autocontenido, saltar.
|
||||
|
||||
### 5. Cerrar la rama (registry)
|
||||
|
||||
```bash
|
||||
# Path portable (cualquier PC): FN_REGISTRY_ROOT si está, si no ~/fn_registry.
|
||||
# Se invoca con `bash` (no `source`): el script tiene un entry point que llama a
|
||||
# tbd_branch_finish con los argumentos cuando se ejecuta directamente, y así
|
||||
# funciona aunque la shell de la sesión sea zsh (evita el fallo de BASH_SOURCE).
|
||||
bash "${FN_REGISTRY_ROOT:-$HOME/fn_registry}/bash/functions/infra/tbd_branch_finish.sh" "<descripción corta del merge>"
|
||||
```
|
||||
|
||||
La función:
|
||||
- Verifica working tree limpio.
|
||||
- Autodetecta `master`/`main`.
|
||||
- `git checkout <base>` + `git pull --rebase`.
|
||||
- `git merge --no-ff <rama> -m "merge: <rama> — <título>"`.
|
||||
- Si conflicto → exit 2, deja al usuario resolver con `git add` + `git commit` + retry.
|
||||
- `git push`.
|
||||
- `git branch -d <rama>`.
|
||||
|
||||
### 6. Confirmar al usuario
|
||||
|
||||
La función ya imprime `Rama '<rama>' integrada a <base> y publicada. Rama local eliminada.` Repetirlo al usuario.
|
||||
|
||||
## Convención de commits
|
||||
|
||||
- `feat:` nueva funcionalidad
|
||||
- `fix:` corrección de error
|
||||
- `refactor:` cambio estructural sin cambio funcional
|
||||
- `docs:` documentación
|
||||
- `chore:` mantenimiento
|
||||
- `test:` tests nuevos o modificados
|
||||
- `merge:` commit de merge (lo genera `tbd_branch_finish` con `--no-ff`)
|
||||
|
||||
## Regla de mensajes
|
||||
|
||||
- Título corto resume el bloque.
|
||||
- Cuerpo en español: qué se cambió, por qué, qué impacto, qué no se tocó.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Cambios commiteados en rama `issue/*` o `quick/*`.
|
||||
- [ ] Cambios distintos en commits diferentes.
|
||||
- [ ] Cada commit con descripción larga en español.
|
||||
- [ ] Tests pasando (o no aplican).
|
||||
- [ ] Feature flags evaluados (o no aplican).
|
||||
- [ ] `tbd_branch_finish` ejecutado con éxito.
|
||||
|
||||
## Para tocar la lógica de cierre
|
||||
|
||||
Editar `tbd_branch_finish_bash_infra` en `bash/functions/infra/tbd_branch_finish.sh`. La parte de tests y commits se queda en este comando porque depende del stack.
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Autogeneracion de objetivo + DoD a partir del primer prompt sustantivo de una
|
||||
# terminal que aun no tiene objetivo. Lo lanza goal_tracker.sh en background (no
|
||||
# bloquea el turno). Usa ask_llm (haiku, API directa; nunca `claude -p`).
|
||||
#
|
||||
# Args: <session_id> <goal_json_file> <prompt_text>
|
||||
|
||||
SID="$1"
|
||||
F="$2"
|
||||
PROMPT="$3"
|
||||
|
||||
# Si ya existe objetivo DEFINITIVO (usuario manual u otro autogen ya termino), no
|
||||
# pisar. Un archivo PROVISIONAL (.provisional=true) SI se pisa: es el placeholder
|
||||
# (= texto del usuario) que pusimos para que el statusline no quede vacio.
|
||||
if [ -f "$F" ] && [ "$(jq -r '.provisional // false' "$F" 2>/dev/null)" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PY="$HOME/fn_registry/python/.venv/bin/python3"
|
||||
ASK="$HOME/fn_registry/python/functions/core/ask_llm.py"
|
||||
[ -x "$PY" ] || exit 0
|
||||
[ -f "$ASK" ] || exit 0
|
||||
|
||||
P=$(printf '%s' "$PROMPT" | tail -c 2000)
|
||||
[ -z "$P" ] && exit 0
|
||||
|
||||
SYS="Dado el PRIMER mensaje de un usuario a un asistente de codigo en una terminal, infiere un OBJETIVO breve de la tarea (maximo 8 palabras, en espanol, sin comillas), un DoD breve (definition of done: condicion concreta de 'terminado', maximo 8 palabras, en espanol) y EXACTAMENTE 3 EMOJIS que representen visualmente la tarea (3 emojis pegados, sin espacios ni texto entre ellos). Responde SOLO un objeto JSON en una sola linea, sin markdown ni texto extra: {\"goal\":\"...\",\"dod\":\"...\",\"emojis\":\"🔭✨🌌\"}. Si el mensaje es un saludo, charla trivial o no describe ninguna tarea, responde exactamente {}."
|
||||
|
||||
RAW=$("$PY" "$ASK" --system "$SYS" "$P" 2>/dev/null)
|
||||
[ -z "$RAW" ] && exit 0
|
||||
|
||||
# Extraer el primer objeto JSON de la salida (tolerante a texto/markdown extra).
|
||||
JSON=$(printf '%s' "$RAW" | tr '\n' ' ' | grep -o '{[^{}]*}' | head -1)
|
||||
[ -z "$JSON" ] && exit 0
|
||||
|
||||
GOAL=$(printf '%s' "$JSON" | jq -r '.goal // ""' 2>/dev/null)
|
||||
DOD=$(printf '%s' "$JSON" | jq -r '.dod // ""' 2>/dev/null)
|
||||
EMOJIS=$(printf '%s' "$JSON" | jq -r '.emojis // ""' 2>/dev/null)
|
||||
[ -z "$GOAL" ] && exit 0
|
||||
|
||||
# Carrera: si entre tanto aparecio un objetivo DEFINITIVO (manual), respetarlo.
|
||||
# Si solo esta el provisional, lo pisamos abajo con el definitivo.
|
||||
if [ -f "$F" ] && [ "$(jq -r '.provisional // false' "$F" 2>/dev/null)" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMP="${F}.tmp.$$"
|
||||
if [ -f "$F" ]; then
|
||||
# Pisar el provisional: fija goal/dod/emojis definitivos y quita el flag,
|
||||
# preservando phase/history/prompts que el provisional ya hubiera acumulado.
|
||||
if jq --arg g "$GOAL" --arg d "$DOD" --arg e "$EMOJIS" \
|
||||
'del(.provisional) | .goal=$g | (if $d != "" then .dod=$d else . end) | (if $e != "" then .emojis=$e else . end)' \
|
||||
"$F" > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$F"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
else
|
||||
if jq -n --arg g "$GOAL" --arg d "$DOD" --arg e "$EMOJIS" --arg p "$P" \
|
||||
'{goal:$g, phase:"planificando", history:["planificando"], prompts:[$p]} | (if $d != "" then .dod=$d else . end) | (if $e != "" then .emojis=$e else . end)' > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$F"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# PostToolUse hook: marca el estado ACTIVO de la tarea segun la herramienta que
|
||||
# el asistente acaba de usar. Determinista, sin LLM, en tiempo real. Solo actua
|
||||
# si la terminal tiene un objetivo fijado.
|
||||
#
|
||||
# El estado de REPOSO (al parar: hecho/pendiente_revision/bloqueado/en_pausa) lo
|
||||
# pone el Stop hook (goal_phase_eval.sh + goal_phase_worker.sh).
|
||||
|
||||
INPUT=$(cat)
|
||||
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
||||
[ -z "$SID" ] && exit 0
|
||||
|
||||
F="$HOME/.claude/goals/${SID}.json"
|
||||
[ -f "$F" ] || exit 0
|
||||
GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null)
|
||||
[ -z "$GOAL" ] && exit 0
|
||||
CUR=$(jq -r '.phase // ""' "$F" 2>/dev/null)
|
||||
|
||||
TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
||||
[ -z "$TOOL" ] && exit 0
|
||||
|
||||
PHASE=""
|
||||
case "$TOOL" in
|
||||
Read|Grep|Glob|NotebookRead|WebFetch|WebSearch|ToolSearch)
|
||||
PHASE=investigando ;;
|
||||
Edit|Write|MultiEdit|NotebookEdit)
|
||||
# Editar tras haber testeado = retoques finales -> puliendo. Si no, es
|
||||
# implementacion normal -> haciendo.
|
||||
case "$CUR" in
|
||||
testeando|puliendo) PHASE=puliendo ;;
|
||||
*) PHASE=haciendo ;;
|
||||
esac
|
||||
;;
|
||||
Task|Agent|Workflow)
|
||||
PHASE=haciendo ;;
|
||||
Bash)
|
||||
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
||||
case "$CMD" in
|
||||
*pytest*|*"go test"*|*ctest*|*jest*|*vitest*|*"npm test"*|*"npm run test"*|*"cargo test"*|*unittest*|*" test "*|*"./fn run"*test*)
|
||||
PHASE=testeando ;;
|
||||
ls|ls\ *|cat\ *|*grep*|find\ *|head\ *|tail\ *|stat\ *|tree*|rg\ *|fd\ *|*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*)
|
||||
PHASE=investigando ;;
|
||||
*)
|
||||
PHASE=haciendo ;;
|
||||
esac
|
||||
;;
|
||||
mcp__registry__fn_search|mcp__registry__fn_show|mcp__registry__fn_code|mcp__registry__fn_uses|mcp__registry__fn_list_domains|mcp__registry__fn_doctor|mcp__registry__fn_proposal)
|
||||
PHASE=investigando ;;
|
||||
mcp__registry__fn_run)
|
||||
PHASE=haciendo ;;
|
||||
TodoWrite|ExitPlanMode|EnterPlanMode|Plan)
|
||||
PHASE=planificando ;;
|
||||
*)
|
||||
# Herramientas que no representan un cambio de actividad (AskUserQuestion,
|
||||
# etc.): no tocar la fase.
|
||||
exit 0 ;;
|
||||
esac
|
||||
[ -z "$PHASE" ] && exit 0
|
||||
|
||||
# Escribir la fase + mantener historial (append solo si cambia respecto al
|
||||
# ultimo; se conservan los ultimos 12 estados).
|
||||
TMP="${F}.tmp.$$"
|
||||
if jq --arg p "$PHASE" '
|
||||
.phase = $p
|
||||
| .history = (
|
||||
( .history // [] ) as $h
|
||||
| ( if ($h | length) > 0 and ($h[-1] == $p) then $h else ($h + [$p]) end )
|
||||
| .[-12:]
|
||||
)
|
||||
' "$F" > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$F"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
exit 0
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Stop hook: tras cada respuesta del asistente, dispara (en background) la
|
||||
# clasificacion de la fase de la tarea. Lee la ultima respuesta del transcript,
|
||||
# la clasifica con ask_llm (haiku) y escribe el resultado en el goal JSON de la
|
||||
# sesion. El statusline lo pinta en el siguiente render.
|
||||
#
|
||||
# No bloquea el cierre del turno: el trabajo pesado va al worker en background.
|
||||
|
||||
INPUT=$(cat)
|
||||
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
||||
TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
|
||||
|
||||
[ -z "$SID" ] && exit 0
|
||||
|
||||
F="$HOME/.claude/goals/${SID}.json"
|
||||
# Solo si esta terminal tiene un objetivo fijado.
|
||||
[ -f "$F" ] || exit 0
|
||||
GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null)
|
||||
[ -z "$GOAL" ] && exit 0
|
||||
|
||||
# Salir del estado ACTIVO de inmediato (sincrono, instantaneo): al parar no debe
|
||||
# quedarse mostrando investigando/haciendo/testeando mientras el worker (haiku,
|
||||
# background) afina el reposo a hecho/pendiente_revision/bloqueado/en_pausa.
|
||||
CUR=$(jq -r '.phase // ""' "$F" 2>/dev/null)
|
||||
case "$CUR" in
|
||||
investigando|planificando|haciendo|testeando|puliendo)
|
||||
TMP="${F}.prov.$$"
|
||||
if jq '.phase="en_pausa"' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F"; else rm -f "$TMP"; fi
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -z "$TRANSCRIPT" ] && exit 0
|
||||
[ -f "$TRANSCRIPT" ] || exit 0
|
||||
|
||||
# Afinar el reposo en background; el hook retorna de inmediato (no bloquea el
|
||||
# turno). El statusline reflejara el valor final en el siguiente refresco.
|
||||
nohup bash "$HOME/.claude/hooks/goal_phase_worker.sh" "$SID" "$TRANSCRIPT" "$F" >/dev/null 2>&1 &
|
||||
exit 0
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
# Worker del Stop hook: resuelve el estado de REPOSO de la tarea cuando el
|
||||
# asistente para y cede el control. Clasifica con ask_llm (haiku, API directa;
|
||||
# nunca `claude -p`, ver regla llm_invocation.md) y lo escribe en el goal JSON
|
||||
# manteniendo el historial.
|
||||
#
|
||||
# El estado ACTIVO (mientras se trabaja: investigando/haciendo/testeando) lo
|
||||
# marca el hook PostToolUse (goal_phase_active.sh), de forma determinista. Este
|
||||
# worker SOLO produce estados de reposo: hecho, pendiente_revision, bloqueado,
|
||||
# en_pausa.
|
||||
#
|
||||
# Args: <session_id> <transcript_path> <goal_json>
|
||||
|
||||
SID="$1"
|
||||
TRANSCRIPT="$2"
|
||||
F="$3"
|
||||
|
||||
PY="$HOME/fn_registry/python/.venv/bin/python3"
|
||||
ASK="$HOME/fn_registry/python/functions/core/ask_llm.py"
|
||||
|
||||
[ -x "$PY" ] || exit 0
|
||||
[ -f "$ASK" ] || exit 0
|
||||
[ -f "$F" ] || exit 0
|
||||
[ -f "$TRANSCRIPT" ] || exit 0
|
||||
|
||||
GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null)
|
||||
[ -z "$GOAL" ] && exit 0
|
||||
CUR=$(jq -r '.phase // ""' "$F" 2>/dev/null)
|
||||
DOD=$(jq -r '.dod // ""' "$F" 2>/dev/null)
|
||||
[ -z "$DOD" ] && DOD="(no definido)"
|
||||
|
||||
is_active() {
|
||||
case "$1" in
|
||||
investigando|planificando|haciendo|testeando|puliendo) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Una pasada de abajo a arriba sobre el turno actual: ultima respuesta de texto
|
||||
# del asistente + ultima peticion del usuario + si hubo trabajo (tool_use).
|
||||
LAST=""
|
||||
USER_MSG=""
|
||||
HAS_WORK=0
|
||||
while IFS= read -r line; do
|
||||
t=$(printf '%s' "$line" | jq -r '.type // empty' 2>/dev/null)
|
||||
if [ "$t" = "assistant" ]; then
|
||||
if [ -z "$LAST" ]; then
|
||||
txt=$(printf '%s' "$line" | jq -r '(.message.content // [])[]? | select(.type=="text") | .text' 2>/dev/null)
|
||||
[ -n "$txt" ] && LAST="$txt"
|
||||
fi
|
||||
if printf '%s' "$line" | jq -e '(.message.content // [])[]? | select(.type=="tool_use")' >/dev/null 2>&1; then
|
||||
HAS_WORK=1
|
||||
fi
|
||||
elif [ "$t" = "user" ]; then
|
||||
ctype=$(printf '%s' "$line" | jq -r '.message.content | type' 2>/dev/null)
|
||||
if [ "$ctype" = "string" ]; then
|
||||
USER_MSG=$(printf '%s' "$line" | jq -r '.message.content' 2>/dev/null)
|
||||
break
|
||||
fi
|
||||
if ! printf '%s' "$line" | jq -e '(.message.content // [])[]? | select(.type=="tool_result")' >/dev/null 2>&1; then
|
||||
USER_MSG=$(printf '%s' "$line" | jq -r '(.message.content // [])[]? | select(.type=="text") | .text' 2>/dev/null)
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done < <(tac "$TRANSCRIPT")
|
||||
|
||||
# Solo resolver reposo si hubo trabajo este turno o si veniamos de un estado
|
||||
# activo (paramos tras currar). Charla sobre un reposo previo: no tocar.
|
||||
if [ "$HAS_WORK" = "0" ] && ! is_active "$CUR"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST=$(printf '%s' "$LAST" | tail -c 4000)
|
||||
[ -z "$LAST" ] && exit 0
|
||||
USER_MSG=$(printf '%s' "$USER_MSG" | tail -c 1500)
|
||||
|
||||
SYS="El asistente acaba de PARAR y cede el control al usuario. Clasifica el estado de REPOSO en que queda la tarea. Responde UNA sola palabra, sin nada mas, de: hecho pendiente_revision preguntando bloqueado en_pausa sin_cambio. hecho=el objetivo esta completo y verificado; pendiente_revision=el asistente termino un trabajo y espera que el humano lo revise o apruebe (no hace una pregunta directa); preguntando=el asistente termina formulando una o varias PREGUNTAS concretas al usuario y necesita su respuesta o decision para continuar; bloqueado=no puede avanzar por un error o por falta de informacion/acceso; en_pausa=hizo un avance y espera la siguiente indicacion, sin estar terminado ni preguntar ni bloqueado; sin_cambio=el turno no altera el estado de reposo actual (charla irrelevante). Distingue: si la respuesta acaba con preguntas al usuario es 'preguntando'; si deja un resultado para que lo mire es 'pendiente_revision'. REGLA DEL DoD: se te da el DoD (definition of done) que define cuando la tarea esta TERMINADA. Marca 'hecho' UNICAMENTE si el resultado descrito por el asistente CUMPLE ese DoD de forma clara y verificada; compara punto por punto el resultado contra el DoD. Si el DoD no se cumple del todo, o no esta verificado, NO uses 'hecho': usa 'pendiente_revision' (dejas un resultado para que el humano lo revise) o 'en_pausa' (avance parcial). Si el DoD es '(no definido)', usa tu criterio: 'hecho' solo si el objetivo esta claramente completo y verificado."
|
||||
|
||||
PROMPT="OBJETIVO DE LA TAREA: ${GOAL}
|
||||
|
||||
DEFINITION OF DONE (DoD) — la tarea esta TERMINADA solo si esto se cumple:
|
||||
${DOD}
|
||||
|
||||
ULTIMA PETICION DEL USUARIO:
|
||||
${USER_MSG}
|
||||
|
||||
ULTIMA RESPUESTA DEL ASISTENTE:
|
||||
${LAST}
|
||||
|
||||
Compara el resultado contra el DoD y responde con una sola palabra de la lista permitida:"
|
||||
|
||||
RAW=$("$PY" "$ASK" --model claude-haiku-4-5-20251001 --system "$SYS" "$PROMPT" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
||||
[ -z "$RAW" ] && exit 0
|
||||
|
||||
case "$RAW" in
|
||||
*sin_cambio*|*sincambio*|*ninguna*|*charla*) exit 0 ;;
|
||||
*pregunt*|*consulta*|*respuesta*) PHASE=preguntando ;;
|
||||
*pendiente*revis*|*revis*|*aprob*) PHASE=pendiente_revision ;;
|
||||
*bloque*) PHASE=bloqueado ;;
|
||||
*hecho*|*complet*|*termin*|*done*) PHASE=hecho ;;
|
||||
*pausa*|*pause*|*siguiente*) PHASE=en_pausa ;;
|
||||
# Si por error devuelve un estado activo al parar, lo tratamos como pausa.
|
||||
investigando|planificando|haciendo|testeando|puliendo) PHASE=en_pausa ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# Escribir la fase + mantener historial (append solo si cambia respecto al
|
||||
# ultimo; se conservan los ultimos 12 estados).
|
||||
TMP="${F}.tmp.$$"
|
||||
if jq --arg p "$PHASE" '
|
||||
.phase = $p
|
||||
| .history = (
|
||||
( .history // [] ) as $h
|
||||
| ( if ($h | length) > 0 and ($h[-1] == $p) then $h else ($h + [$p]) end )
|
||||
| .[-12:]
|
||||
)
|
||||
' "$F" > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$F"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
exit 0
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# DESACTIVADO (2026-06-21): este hook regeneraba el campo `.dod` (movil) del
|
||||
# goal.json llamando a un LLM (haiku via ask_llm.py) en CADA prompt de CADA
|
||||
# sesion. Con muchas sesiones de la flota activas a la vez eso amplificaba el
|
||||
# rate-limit compartido de la organizacion ("Server is temporarily limiting
|
||||
# requests"). Una request API por turno por agente = coste innecesario.
|
||||
#
|
||||
# El `.dod` movil NO lo consume nadie: el parser de la flota
|
||||
# (functions/infra/list_claude_fleet.go, struct goalFile/readGoal) solo lee
|
||||
# goal/phase/emojis/rename/dod_contract/dod_status/role; ignora `.dod` por
|
||||
# completo. El criterio de aceptacion real que clasifica la flota es
|
||||
# `dod_contract` + `dod_status`, escrito por set_dod_contract.py (sin LLM) y
|
||||
# consumido por ClassifyFleetTermination. Ese sistema queda intacto.
|
||||
#
|
||||
# Por tanto la regeneracion del `.dod` movil con haiku se elimina por completo:
|
||||
# cero llamadas LLM por prompt. El objetivo+DoD inicial los sigue generando
|
||||
# goal_autogen.sh una sola vez por terminal (junto con goal/emojis, que si se
|
||||
# usan); el usuario puede ajustar el DoD a mano con "dod: ...".
|
||||
#
|
||||
# Se conserva el archivo como no-op para no romper ningun disparador historico
|
||||
# (defensa en profundidad). El disparo desde goal_tracker.sh tambien se retiro.
|
||||
exit 0
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
# UserPromptSubmit hook del sistema de objetivo+fase por terminal.
|
||||
#
|
||||
# Modelo:
|
||||
# - El OBJETIVO (target) es el IDENTIFICATIVO de la terminal: se genera una vez
|
||||
# (del primer prompt, o a mano con "objetivo: ...") y NUNCA cambia solo.
|
||||
# - El DoD SI se ajusta con tus prompts para reflejar la condicion de terminado.
|
||||
# - La FASE la mantienen los hooks de fase: PostToolUse (activo) y Stop (reposo).
|
||||
#
|
||||
# Comandos META (se ejecutan FUERA DE BANDA: el hook hace su efecto y BLOQUEA el
|
||||
# prompt con decision=block, asi el agente NO lo recibe ni responde; solo ves una
|
||||
# confirmacion breve):
|
||||
# objetivo: <texto> fija/cambia el objetivo a mano (meta:/goal: equivalen).
|
||||
# objetivo: clear lo borra (tambien -, none, borrar, quitar, reset).
|
||||
# dod: <texto> fija un DoD a mano.
|
||||
# dod: clear lo borra.
|
||||
# pausa marca la fase en en_pausa (Ctrl-C no dispara hooks).
|
||||
|
||||
INPUT=$(cat)
|
||||
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
||||
[ -z "$SID" ] && exit 0
|
||||
|
||||
F="$HOME/.claude/goals/${SID}.json"
|
||||
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // ""' 2>/dev/null)
|
||||
PROMPT_TRIM=$(printf '%s' "$PROMPT" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
|
||||
|
||||
# Bloquea el prompt (no llega al agente) y muestra <reason> al usuario.
|
||||
block() { jq -n --arg r "$1" '{decision:"block", reason:$r}'; exit 0; }
|
||||
|
||||
# --- objetivo: <texto> (manual; preserva el DoD si ya existia) ---
|
||||
GOAL_LINE=$(printf '%s' "$PROMPT" | grep -ioE '^[[:space:]]*(objetivo|meta|goal)[[:space:]]*:[[:space:]]*.+' | head -1)
|
||||
if [ -n "$GOAL_LINE" ]; then
|
||||
NEWGOAL=$(printf '%s' "$GOAL_LINE" | sed -E 's/^[^:]*:[[:space:]]*//; s/[[:space:]]+$//')
|
||||
case "$NEWGOAL" in
|
||||
-|clear|none|borrar|quitar|reset)
|
||||
rm -f "$F"
|
||||
block "🎯 Objetivo de esta terminal borrado." ;;
|
||||
esac
|
||||
if [ -f "$F" ]; then
|
||||
PH=$(jq -r '.phase // "planificando"' "$F" 2>/dev/null)
|
||||
DD=$(jq -r '.dod // ""' "$F" 2>/dev/null)
|
||||
else
|
||||
PH="planificando"; DD=""
|
||||
fi
|
||||
TMP="${F}.tmp.$$"
|
||||
if jq -n --arg g "$NEWGOAL" --arg p "$PH" --arg d "$DD" \
|
||||
'{goal:$g, phase:$p, prompts:[]} | if $d != "" then .dod=$d else . end' > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$F"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
block "🎯 Objetivo fijado: ${NEWGOAL}"
|
||||
fi
|
||||
|
||||
# Nota: el rename de FleetView se hace ahora con alt+r DENTRO de la TUI (escribe
|
||||
# el campo .rename del goal directamente). Ya no se captura /rename en este hook,
|
||||
# asi el built-in /rename de Claude Code queda libre para renombrar la sesion.
|
||||
|
||||
# --- dod: <texto> ---
|
||||
DOD_LINE=$(printf '%s' "$PROMPT" | grep -ioE '^[[:space:]]*dod[[:space:]]*:[[:space:]]*.+' | head -1)
|
||||
if [ -n "$DOD_LINE" ]; then
|
||||
NEWDOD=$(printf '%s' "$DOD_LINE" | sed -E 's/^[^:]*:[[:space:]]*//; s/[[:space:]]+$//')
|
||||
[ -f "$F" ] || block "Fija primero un objetivo (\"objetivo: ...\") antes del DoD."
|
||||
case "$NEWDOD" in
|
||||
-|clear|none|borrar|quitar|reset)
|
||||
TMP="${F}.tmp.$$"
|
||||
jq 'del(.dod)' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F"
|
||||
block "🏁 DoD borrado." ;;
|
||||
esac
|
||||
TMP="${F}.tmp.$$"
|
||||
if jq --arg d "$NEWDOD" '.dod=$d' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F"; else rm -f "$TMP"; fi
|
||||
block "🏁 DoD fijado: ${NEWDOD}"
|
||||
fi
|
||||
|
||||
# --- pausa (marca manual; Ctrl-C no dispara hooks en Claude Code) ---
|
||||
case "$PROMPT_TRIM" in
|
||||
pausa|pause|pausar|"en pausa"|/pausa)
|
||||
[ -f "$F" ] || block "No hay objetivo en esta terminal."
|
||||
TMP="${F}.tmp.$$"
|
||||
if jq '.phase="en_pausa" | .history=((.history // [])+["en_pausa"])[-12:]' "$F" > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$F"
|
||||
fi
|
||||
block "⏸️ Fase marcada en pausa." ;;
|
||||
esac
|
||||
|
||||
# --- prompt NORMAL: pasa al agente + estado ---
|
||||
# Distinguimos dos situaciones por el flag .provisional del goal file:
|
||||
# - no existe el archivo -> primer prompt: ponemos objetivo PROVISIONAL = tu
|
||||
# texto + lanzamos autogen (haiku, UNA sola vez)
|
||||
# que lo definira.
|
||||
# - existe pero .provisional -> autogen aun no termino (o fallo): conservamos el
|
||||
# provisional y relanzamos autogen (idempotente,
|
||||
# self-healing).
|
||||
# - existe y NO provisional -> objetivo definitivo: solo mostramos estado.
|
||||
#
|
||||
# NOTA (2026-06-21): el campo `.dod` movil YA NO se regenera con LLM en cada
|
||||
# prompt. goal_refine.sh esta desactivado (era una request haiku por turno por
|
||||
# sesion -> amplificaba el rate-limit compartido de la organizacion). El `.dod`
|
||||
# movil no lo consume nadie; el criterio que clasifica la flota es `dod_contract`
|
||||
# + `dod_status` (set_dod_contract.py, sin LLM). El DoD inicial lo fija autogen
|
||||
# una vez; el usuario lo ajusta a mano con "dod: ...".
|
||||
PROV="false"
|
||||
GOAL_NOW=""
|
||||
if [ -f "$F" ]; then
|
||||
PROV=$(jq -r '.provisional // false' "$F" 2>/dev/null)
|
||||
GOAL_NOW=$(jq -r '.goal // ""' "$F" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# "Objetivo definitivo" = archivo con goal NO vacio y no provisional. El check de
|
||||
# goal no vacio es clave para los ejecutores lanzados por spawn_fleet_agent: su
|
||||
# goal.json se PRE-CREA con solo {role, parent_orchestrator} (sin goal). Sin este
|
||||
# guard, el hook tomaria ese archivo como objetivo definitivo y nunca lanzaria
|
||||
# autogen, dejando el goal vacio para siempre (statusline y FleetView sin objetivo).
|
||||
if [ -f "$F" ] && [ "$PROV" != "true" ] && [ -n "$GOAL_NOW" ]; then
|
||||
G=$(jq -r '.goal // ""' "$F" 2>/dev/null)
|
||||
P=$(jq -r '.phase // ""' "$F" 2>/dev/null)
|
||||
D=$(jq -r '.dod // ""' "$F" 2>/dev/null)
|
||||
echo "GOAL-TRACKER: file=$F | goal=\"$G\" dod=\"$D\" phase=\"$P\". El objetivo es fijo (identificativo de la terminal, NO lo cambies). El DoD inicial lo fija el autogen una vez (sin LLM por prompt); el usuario lo ajusta con \"dod: ...\" — NO lo regeneres tu. La fase la mantienen los hooks (PostToolUse=activo, Stop=reposo) — NO la escribas. Comandos meta del usuario (no los uses tu): objetivo:/dod:/pausa."
|
||||
else
|
||||
# Sin objetivo definitivo todavia. Mostramos de inmediato un objetivo PROVISIONAL
|
||||
# igual a tu propio texto (truncado), para que el statusline no quede vacio
|
||||
# mientras haiku genera el real en background. autogen pisara este provisional
|
||||
# con el definitivo al terminar (su guard respeta .provisional).
|
||||
if [ "${#PROMPT_TRIM}" -ge 12 ]; then
|
||||
TMP="${F}.tmp.$$"
|
||||
PROV_GOAL=$(printf '%s' "$PROMPT_TRIM" | head -c 70)
|
||||
if [ -n "$GOAL_NOW" ]; then
|
||||
# Ya habia goal provisional: conserva su goal, solo acumula el prompt.
|
||||
jq --arg p "$PROMPT_TRIM" '.prompts = ((.prompts // []) + [$p])[-12:]' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F" || rm -f "$TMP"
|
||||
elif [ -f "$F" ]; then
|
||||
# Archivo PRE-CREADO por spawn_fleet_agent ({role, parent_orchestrator})
|
||||
# sin goal: fija el provisional PRESERVANDO los campos existentes (role,
|
||||
# parent_orchestrator) y deja que autogen lo pise con el definitivo.
|
||||
jq --arg g "$PROV_GOAL" --arg p "$PROMPT_TRIM" \
|
||||
'. + {goal:$g, phase:"planificando", history:["planificando"], prompts:[$p], provisional:true}' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F" || rm -f "$TMP"
|
||||
else
|
||||
jq -n --arg g "$PROV_GOAL" --arg p "$PROMPT_TRIM" \
|
||||
'{goal:$g, phase:"planificando", history:["planificando"], prompts:[$p], provisional:true}' > "$TMP" 2>/dev/null && mv "$TMP" "$F" || rm -f "$TMP"
|
||||
fi
|
||||
nohup bash "$HOME/.claude/hooks/goal_autogen.sh" "$SID" "$F" "$PROMPT" >/dev/null 2>&1 &
|
||||
fi
|
||||
echo "GOAL-TRACKER: file=$F (objetivo PROVISIONAL = tu texto; generando el objetivo+DoD real con haiku en background). El usuario tambien puede fijarlo con \"objetivo: ...\" / \"dod: ...\"."
|
||||
fi
|
||||
exit 0
|
||||
+84
-2
@@ -1,8 +1,90 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Edit(~/.claude/**)",
|
||||
"Write(~/.claude/**)",
|
||||
"Edit(.claude/**)",
|
||||
"Write(.claude/**)",
|
||||
"Bash(CGO_ENABLED=1 go test *)",
|
||||
"Bash(sqlite3 *)",
|
||||
"Read(//home/enmanuel/.claude/**)"
|
||||
],
|
||||
"deny": [
|
||||
"Edit(~/.claude/.git/**)",
|
||||
"Write(~/.claude/.git/**)",
|
||||
"Edit(.git/**)",
|
||||
"Write(.git/**)"
|
||||
],
|
||||
"defaultMode": "dontAsk"
|
||||
},
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "~/.claude/hooks/goal_tracker.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Notification": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "~/.claude/hooks/goal_notify.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "~/.claude/hooks/goal_phase_eval.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "~/.claude/hooks/goal_phase_active.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "~/.claude/statusline.sh",
|
||||
"padding": 1
|
||||
"padding": 1,
|
||||
"refreshInterval": 2
|
||||
},
|
||||
"skipDangerousModePermissionPrompt": true
|
||||
"enabledPlugins": {
|
||||
"gopls-lsp@claude-plugins-official": true,
|
||||
"caveman@caveman": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"caveman": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "JuliusBrussee/caveman"
|
||||
}
|
||||
}
|
||||
},
|
||||
"language": "Español",
|
||||
"effortLevel": "xhigh",
|
||||
"voice": {
|
||||
"enabled": true,
|
||||
"mode": "hold"
|
||||
},
|
||||
"skipDangerousModePermissionPrompt": true,
|
||||
"preferredNotifChannel": "notifications_disabled",
|
||||
"agentPushNotifEnabled": false,
|
||||
"voiceEnabled": true
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
name: auto-create
|
||||
description: Crea un issue nuevo e integra automáticamente SIN pedir confirmación
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# auto-create
|
||||
|
||||
Crea un issue nuevo y lo integra automáticamente **sin pedir confirmación**.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/auto-create
|
||||
```
|
||||
|
||||
## Diferencia con /create-issue
|
||||
|
||||
Este comando NO pausa para confirmación. Solicita datos pero integra automáticamente.
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1-7. Crear issue (igual que /create-issue)
|
||||
|
||||
1. Determinar número
|
||||
2. Solicitar inputs (titulo, descripción)
|
||||
3. Generar slug
|
||||
4. Evaluar tamaño
|
||||
5. Crear desde template
|
||||
6. Feature flag (si aplica)
|
||||
7. Actualizar índice
|
||||
|
||||
**Sin confirmación** - continuar directamente.
|
||||
|
||||
### 8. Integración automática
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
git checkout -b quick/create-issue-<NNNN>
|
||||
|
||||
# Commit
|
||||
git add dev/issues/<NNNN>*.md dev/issues/README.md
|
||||
git commit -m "docs: crear issue <NNNN>-<slug>"
|
||||
|
||||
# Si multi-issue, commit de feature flag
|
||||
git add dev/feature_flags.json
|
||||
git commit -m "feat: agregar feature flag <nombre>"
|
||||
|
||||
# Tests (si aplican)
|
||||
go test -tags goolm ./...
|
||||
|
||||
# Merge
|
||||
git checkout master
|
||||
git merge --no-ff quick/create-issue-<NNNN>
|
||||
git push
|
||||
git branch -d quick/create-issue-<NNNN>
|
||||
```
|
||||
|
||||
### 9. Mostrar resultado
|
||||
|
||||
```
|
||||
Issue <NNNN>-<slug> creado e integrado automáticamente
|
||||
|
||||
Para implementar:
|
||||
/fix-issue <NNNN>
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Sin confirmación
|
||||
- Mismo formato que /create-issue
|
||||
- Trunk-based con rama quick/
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
name: auto-fix
|
||||
description: Implementa un issue completo automáticamente SIN pedir confirmación
|
||||
argument-hint: <NNNN>
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit, TodoWrite
|
||||
---
|
||||
|
||||
# auto-fix
|
||||
|
||||
Implementa un issue completo automáticamente **sin pedir confirmación** antes de integrar.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/auto-fix <NNNN>
|
||||
/auto-fix <NNNN>-<slug>
|
||||
```
|
||||
|
||||
## Diferencia con /fix-issue
|
||||
|
||||
Este comando NO pausa para confirmación. Ejecuta todo el flujo automáticamente.
|
||||
|
||||
**Usar cuando:** estés completamente seguro de que el issue puede implementarse automáticamente.
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1-8. Implementar (igual que /fix-issue)
|
||||
|
||||
1. Resolver issue objetivo
|
||||
2. Leer issue completo
|
||||
3. Crear rama `issue/<NNNN>-<slug>`
|
||||
4. Planificar con TodoWrite
|
||||
5. Implementar completo
|
||||
6. Tests obligatorios
|
||||
7. Feature flags (si aplica)
|
||||
8. Cerrar issue
|
||||
|
||||
**Sin confirmación** - continuar directamente.
|
||||
|
||||
### 9. Integración automática
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
go test -tags goolm ./... # verificación final
|
||||
git merge --no-ff issue/<NNNN>-<slug> -m "merge: issue/<NNNN>-<slug>"
|
||||
git push
|
||||
git branch -d issue/<NNNN>-<slug>
|
||||
```
|
||||
|
||||
### 10. Mostrar resultado
|
||||
|
||||
```
|
||||
Issue <NNNN> completado e integrado automáticamente
|
||||
|
||||
Commits integrados: N
|
||||
Tests: pasando
|
||||
Issue: movido a completed/
|
||||
|
||||
NOTA: Integración automática sin confirmación.
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Sin confirmación (diferencia clave)
|
||||
- Misma calidad que /fix-issue
|
||||
- STOP si tests fallan
|
||||
|
||||
## Reglas
|
||||
|
||||
- NO pedir confirmación
|
||||
- MISMA calidad que /fix-issue
|
||||
- STOP si tests fallan (no integrar código roto)
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
name: cleanup-worktrees
|
||||
description: Limpia worktrees y ramas locales después de merge
|
||||
argument-hint: <issue_number> | --all
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# cleanup-worktrees
|
||||
|
||||
Elimina worktrees y sus ramas locales asociadas después de haber sido mergeadas.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/cleanup-worktrees <NNNN> # Limpiar worktree específico
|
||||
/cleanup-worktrees --all # Limpiar todos
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Validar argumentos
|
||||
|
||||
- Número de issue (4 dígitos): limpiar ese worktree
|
||||
- `--all`: limpiar todos en `worktrees/`
|
||||
|
||||
### 2. Determinar worktrees a limpiar
|
||||
|
||||
```bash
|
||||
# Para issue específica
|
||||
WORKTREE_PATH="worktrees/issue-$ISSUE_NUM"
|
||||
|
||||
# Para --all
|
||||
find worktrees -maxdepth 1 -type d -name "issue-*"
|
||||
```
|
||||
|
||||
### 3. Confirmar con usuario
|
||||
|
||||
```
|
||||
Se eliminarán:
|
||||
- worktrees/issue-0003 (rama: quick/fix-issue-0003)
|
||||
|
||||
¿Continuar? (y/N):
|
||||
```
|
||||
|
||||
### 4. Limpiar cada worktree
|
||||
|
||||
Para cada uno:
|
||||
1. Verificar si rama fue mergeada
|
||||
2. Si NO mergeada: advertir y preguntar
|
||||
3. Eliminar worktree: `git worktree remove <path> --force`
|
||||
4. Eliminar rama: `git branch -D <branch>`
|
||||
|
||||
### 5. Reportar resultado
|
||||
|
||||
```
|
||||
Limpieza completada
|
||||
|
||||
Worktrees restantes:
|
||||
(ninguno)
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Nomenclatura worktrees: `worktrees/issue-NNNN`
|
||||
- Nomenclatura ramas: `quick/fix-issue-NNNN`
|
||||
- Confirmación interactiva siempre
|
||||
|
||||
## Reglas
|
||||
|
||||
- SIEMPRE verificar merge antes de eliminar
|
||||
- NUNCA eliminar sin confirmación
|
||||
- SIEMPRE usar --force en worktree remove
|
||||
@@ -1,439 +0,0 @@
|
||||
---
|
||||
name: create-agent
|
||||
description: Crea un nuevo agente especializado en .claude/agents/ con su SKILL.md y estructura completa
|
||||
argument-hint: [nombre]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit, AskUserQuestion
|
||||
---
|
||||
|
||||
# create-agent
|
||||
|
||||
Crea un nuevo agente especializado en `.claude/agents/` con archivo `SKILL.md` obligatorio siguiendo la estructura oficial de Claude Code.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/create-agent [nombre]
|
||||
/create-agent api-client
|
||||
/create-agent cloud-deploy
|
||||
```
|
||||
|
||||
## Precondiciones
|
||||
|
||||
- [ ] Carpeta `.claude/agents/` existe
|
||||
- [ ] No existe agente con el mismo nombre
|
||||
- [ ] Nombre cumple convenciones (minúsculas, guiones)
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Validar nombre
|
||||
|
||||
- Solo minúsculas, números y guiones
|
||||
- No nombres reservados (help, clear, exit)
|
||||
- Máximo 64 caracteres
|
||||
|
||||
```bash
|
||||
ls -d .claude/agents/*/ 2>/dev/null | xargs -n1 basename | grep -E "^${nombre}$"
|
||||
```
|
||||
|
||||
Si existe, STOP.
|
||||
|
||||
### 2. Solicitar inputs usando AskUserQuestion
|
||||
|
||||
Usar `AskUserQuestion` para obtener:
|
||||
|
||||
#### Input 1: Información básica
|
||||
- **nombre**: minúsculas y guiones (ej: `api-client`)
|
||||
- **descripcion**: qué hace el agente y cuándo invocarlo (1-2 frases claras)
|
||||
|
||||
#### Input 2: Configuración técnica
|
||||
- **model**: Modelo Claude a usar
|
||||
- `sonnet` (default): Balance costo/capacidad
|
||||
- `opus`: Tareas complejas que requieren máximo razonamiento
|
||||
- `haiku`: Tareas simples y rápidas
|
||||
|
||||
- **tools**: Herramientas necesarias (separadas por coma)
|
||||
- Default: `Read, Write, Bash, Glob, Grep, Edit`
|
||||
- Opcionales: `WebFetch, WebSearch, NotebookEdit`
|
||||
|
||||
#### Input 3: Configuración de proyecto
|
||||
- **gestiona_repo**: ¿Gestiona un repositorio local?
|
||||
- `si`: Crear carpeta en `~/.local_agentes/`
|
||||
- `no`: Solo definición de agente
|
||||
|
||||
- **usa_mcp**: ¿Usa MCP servers? (gitea, sqlite, etc)
|
||||
- `si`: Solicitar configuración de MCP
|
||||
- `no`: Omitir mcpServers
|
||||
|
||||
#### Input 4: MCP Servers (si usa_mcp = si)
|
||||
Preguntar qué MCP servers necesita:
|
||||
- `gitea`: Gestión de repositorios Gitea
|
||||
- `sqlite`: Bases de datos SQLite
|
||||
- `filesystem`: Sistema de archivos
|
||||
- `otro`: Configuración personalizada
|
||||
|
||||
#### Input 5: Documentación
|
||||
- **rol**: Rol del agente (ej: "Eres un experto en...")
|
||||
- **capacidades**: Lista de capacidades principales
|
||||
- **flujo_trabajo**: Descripción del flujo de trabajo típico
|
||||
- **ejemplos_uso**: Ejemplos de cuándo invocar al agente
|
||||
|
||||
### 3. Crear carpeta del agente
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/agents/${nombre}
|
||||
```
|
||||
|
||||
### 4. Crear carpeta local (si gestiona_repo = si)
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local_agentes/${nombre}
|
||||
```
|
||||
|
||||
### 5. Generar frontmatter YAML
|
||||
|
||||
Estructura base:
|
||||
```yaml
|
||||
---
|
||||
name: ${nombre}
|
||||
description: ${descripcion}
|
||||
model: ${model}
|
||||
tools: ${tools}
|
||||
mcpServers: # Solo si usa_mcp = si
|
||||
- ${mcp_config}
|
||||
---
|
||||
```
|
||||
|
||||
### 6. Generar SKILL.md completo
|
||||
|
||||
Template oficial de agente:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: ${nombre}
|
||||
description: ${descripcion}
|
||||
model: ${model}
|
||||
tools: ${tools}
|
||||
${mcp_servers_section}
|
||||
---
|
||||
|
||||
# Agente ${nombre}
|
||||
|
||||
${rol}
|
||||
|
||||
## Tu entorno
|
||||
|
||||
${descripcion_entorno}
|
||||
|
||||
## Capacidades principales
|
||||
|
||||
${capacidades}
|
||||
|
||||
## Flujo de trabajo
|
||||
|
||||
${flujo_trabajo}
|
||||
|
||||
## Templates disponibles
|
||||
|
||||
${templates_codigo}
|
||||
|
||||
## Integración con otros agentes
|
||||
|
||||
${integracion}
|
||||
|
||||
## Ejemplos de uso
|
||||
|
||||
${ejemplos_uso}
|
||||
|
||||
## Comandos útiles
|
||||
|
||||
${comandos}
|
||||
|
||||
## Notas y convenciones
|
||||
|
||||
${notas}
|
||||
```
|
||||
|
||||
### 7. Templates de MCP Servers
|
||||
|
||||
#### Gitea MCP
|
||||
```yaml
|
||||
mcpServers:
|
||||
- gitea:
|
||||
type: stdio
|
||||
command: gitea-mcp
|
||||
args:
|
||||
- -t
|
||||
- stdio
|
||||
- --host
|
||||
- "${GITEA_URL}"
|
||||
- --token
|
||||
- "${GITEA_TOKEN}"
|
||||
```
|
||||
|
||||
#### SQLite MCP
|
||||
```yaml
|
||||
mcpServers:
|
||||
- sqlite:
|
||||
type: stdio
|
||||
command: sqlite-mcp
|
||||
args:
|
||||
- --db
|
||||
- "${DB_PATH}"
|
||||
```
|
||||
|
||||
#### Filesystem MCP
|
||||
```yaml
|
||||
mcpServers:
|
||||
- filesystem:
|
||||
type: stdio
|
||||
command: mcp-server-filesystem
|
||||
args:
|
||||
- --allowed-directories
|
||||
- "${ALLOWED_DIR}"
|
||||
```
|
||||
|
||||
### 8. Mostrar y confirmar
|
||||
|
||||
```
|
||||
Agente creado: ${nombre}
|
||||
Ubicación: .claude/agents/${nombre}/SKILL.md
|
||||
${carpeta_local ? "Carpeta local: ~/.local_agentes/" + nombre : ""}
|
||||
|
||||
Configuración:
|
||||
- Model: ${model}
|
||||
- Tools: ${tools}
|
||||
- MCP: ${usa_mcp ? "Sí" : "No"}
|
||||
- Repositorio local: ${gestiona_repo ? "Sí" : "No"}
|
||||
|
||||
¿Te parece bien?
|
||||
- Si correcto: commit e integrar automáticamente
|
||||
- Si ajustes: edita manualmente antes de integrar
|
||||
```
|
||||
|
||||
### 9. Crear README.md en carpeta local (si gestiona_repo = si)
|
||||
|
||||
```bash
|
||||
cat > ~/.local_agentes/${nombre}/README.md <<EOF
|
||||
# ${nombre}
|
||||
|
||||
${descripcion}
|
||||
|
||||
## Estructura del proyecto
|
||||
|
||||
\`\`\`
|
||||
~/.local_agentes/${nombre}/
|
||||
├── README.md
|
||||
├── CLAUDE.md # Instrucciones para Claude
|
||||
└── ... # Archivos del proyecto
|
||||
\`\`\`
|
||||
|
||||
## Sincronización con Gitea
|
||||
|
||||
\`\`\`bash
|
||||
cd ~/.local_agentes/${nombre}
|
||||
git remote add origin \${GITEA_URL}/\${user}/${nombre}.git
|
||||
git push -u origin main
|
||||
\`\`\`
|
||||
|
||||
## Uso
|
||||
|
||||
Invocar con:
|
||||
\`\`\`
|
||||
Hablar con el agente ${nombre} para [tarea]
|
||||
\`\`\`
|
||||
EOF
|
||||
```
|
||||
|
||||
### 10. Ejecutar /git-push
|
||||
|
||||
Si confirma, crear rama `quick/create-agent-${nombre}` e integrar.
|
||||
|
||||
### 11. Verificar disponibilidad
|
||||
|
||||
```
|
||||
Agente "${nombre}" creado e integrado
|
||||
|
||||
El agente está disponible en:
|
||||
.claude/agents/${nombre}/SKILL.md
|
||||
|
||||
${gestiona_repo ? "Carpeta de trabajo:\n ~/.local_agentes/" + nombre : ""}
|
||||
|
||||
Para usar, solicita al usuario:
|
||||
"Trabajar con el agente ${nombre} para [tarea]"
|
||||
|
||||
Configuración:
|
||||
- Model: ${model}
|
||||
- Tools: ${tools}
|
||||
- MCP Servers: ${usa_mcp ? "Sí" : "No"}
|
||||
```
|
||||
|
||||
## Campos del frontmatter de agentes
|
||||
|
||||
| Campo | Descripción | Requerido |
|
||||
|-------|-------------|-----------|
|
||||
| name | Nombre del agente | Sí |
|
||||
| description | Qué hace y cuándo invocarlo | Sí |
|
||||
| model | Model Claude (sonnet, opus, haiku) | Sí |
|
||||
| tools | Herramientas disponibles | Sí |
|
||||
| mcpServers | Servidores MCP (opcional) | No |
|
||||
|
||||
## Estructura de documentación de agentes
|
||||
|
||||
Seguir este orden en el contenido Markdown:
|
||||
|
||||
1. **Título y rol**: Descripción del rol del agente
|
||||
2. **Tu entorno**: Dónde trabaja, qué repositorios gestiona
|
||||
3. **Capacidades principales**: Lista de lo que puede hacer
|
||||
4. **Flujo de trabajo**: Cómo abordar tareas típicas
|
||||
5. **Templates disponibles**: Código de ejemplo
|
||||
6. **Integración con otros agentes**: Cómo colabora con otros
|
||||
7. **Ejemplos de uso**: Cuándo invocar al agente
|
||||
8. **Comandos útiles**: Comandos CLI relevantes
|
||||
9. **Notas y convenciones**: Reglas y mejores prácticas
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Nombres descriptivos con guiones (ej: `api-client`, `cloud-deploy`)
|
||||
- Descripciones claras para invocación automática por Claude
|
||||
- Un agente por dominio/especialización
|
||||
- Documentación completa con ejemplos prácticos
|
||||
- Templates de código cuando sea aplicable
|
||||
|
||||
## Diferencia entre Agentes y Skills
|
||||
|
||||
| Característica | Agente | Skill |
|
||||
|----------------|--------|-------|
|
||||
| Ubicación | `.claude/agents/` | `.claude/skills/` |
|
||||
| Propósito | Experto especializado | Automatización/herramienta |
|
||||
| Invocación | Claude decide cuándo | Usuario con `/nombre` |
|
||||
| Contenido | Conocimiento de dominio | Flujo de trabajo |
|
||||
| Ejemplo | `backend-lib`, `docker` | `git-push`, `create-issue` |
|
||||
|
||||
## Ejemplos de agentes
|
||||
|
||||
### Agente simple (sin repo ni MCP)
|
||||
```yaml
|
||||
---
|
||||
name: code-review
|
||||
description: Agente para revisar código y sugerir mejoras
|
||||
model: sonnet
|
||||
tools: Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Agente Code Review
|
||||
|
||||
Eres un experto en revisión de código...
|
||||
```
|
||||
|
||||
### Agente complejo (con repo y MCP)
|
||||
```yaml
|
||||
---
|
||||
name: api-client
|
||||
description: Agente para generar clientes API desde especificaciones OpenAPI
|
||||
model: sonnet
|
||||
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
mcpServers:
|
||||
- gitea:
|
||||
type: stdio
|
||||
command: gitea-mcp
|
||||
args:
|
||||
- -t
|
||||
- stdio
|
||||
- --host
|
||||
- "${GITEA_URL}"
|
||||
- --token
|
||||
- "${GITEA_TOKEN}"
|
||||
---
|
||||
|
||||
# Agente API Client
|
||||
|
||||
Eres un experto en generación de clientes API...
|
||||
|
||||
## Tu entorno
|
||||
|
||||
- **Repositorio**: `Bl4cksmith/api-clients` (Gitea)
|
||||
- **Carpeta local**: `~/.local_agentes/api-client`
|
||||
- **Stack**: TypeScript, Go, Python
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## Reglas
|
||||
|
||||
- Validar nombre antes de crear
|
||||
- SKILL.md es obligatorio
|
||||
- Confirmación antes de integrar
|
||||
- Crear carpeta local solo si gestiona_repo = si
|
||||
- MCP servers solo si usa_mcp = si
|
||||
- Documentación completa y con ejemplos
|
||||
|
||||
## Integración con otros agentes
|
||||
|
||||
### Con gitea
|
||||
```bash
|
||||
# Crear repositorio para el agente (si gestiona_repo = si)
|
||||
cd ~/.local_agentes/${nombre}
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
# Usar agente gitea para crear repo y push
|
||||
```
|
||||
|
||||
### Con backend-lib / frontend-lib
|
||||
```markdown
|
||||
# En SKILL.md del nuevo agente, documentar integración:
|
||||
|
||||
## Integración con otros agentes
|
||||
|
||||
### Con backend-lib (DevFactory)
|
||||
- Usar DevFactory para estructuras funcionales Go
|
||||
- Integrar via go.work
|
||||
|
||||
### Con frontend-lib
|
||||
- Usar Frontend_Library para componentes React
|
||||
- Integrar via pnpm link
|
||||
```
|
||||
|
||||
## Variables dinámicas
|
||||
|
||||
| Variable | Descripción |
|
||||
|----------|-------------|
|
||||
| ${nombre} | Nombre del agente |
|
||||
| ${descripcion} | Descripción del agente |
|
||||
| ${model} | Model Claude |
|
||||
| ${tools} | Herramientas disponibles |
|
||||
| ${CLAUDE_SKILL_DIR} | Ruta del skill |
|
||||
|
||||
## Flujo completo de ejemplo
|
||||
|
||||
```bash
|
||||
# Usuario invoca
|
||||
/create-agent api-client
|
||||
|
||||
# Skill valida nombre
|
||||
✓ Nombre válido
|
||||
|
||||
# Skill pregunta configuración con AskUserQuestion
|
||||
? Descripción: Agente para generar clientes API desde OpenAPI
|
||||
? Model: sonnet
|
||||
? Tools: Read, Write, Bash, Glob, Grep, Edit
|
||||
? ¿Gestiona repositorio?: Sí
|
||||
? ¿Usa MCP?: Sí
|
||||
? MCP servers: gitea
|
||||
|
||||
# Skill crea estructura
|
||||
✓ Carpeta creada: .claude/agents/api-client/
|
||||
✓ Carpeta local creada: ~/.local_agentes/api-client/
|
||||
✓ SKILL.md generado
|
||||
✓ README.md generado
|
||||
|
||||
# Skill confirma
|
||||
Agente "api-client" creado
|
||||
|
||||
# Skill integra con git
|
||||
✓ Rama: quick/create-agent-api-client
|
||||
✓ Commit: "feat: crear agente api-client"
|
||||
✓ Push exitoso
|
||||
```
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
name: create-issue
|
||||
description: Crea un issue nuevo en dev/issues/ con confirmación del usuario
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# create-issue
|
||||
|
||||
Crea un issue nuevo con estructura completa. Si es grande, lo desglosa en sub-issues con feature flags.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/create-issue
|
||||
```
|
||||
|
||||
## Precondiciones
|
||||
|
||||
- [ ] Directorio `dev/issues/` existe
|
||||
- [ ] Template `.claude/templates/issue.md` existe
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Determinar número del issue
|
||||
|
||||
```bash
|
||||
ls dev/issues/ dev/issues/completed/ | grep -oP '^\d{4}' | sort -rn | head -1
|
||||
```
|
||||
|
||||
Próximo issue = número_más_alto + 1 (formato 4 dígitos)
|
||||
|
||||
### 2. Solicitar inputs
|
||||
|
||||
- `titulo`: título corto y descriptivo
|
||||
- `descripcion`: objetivo de lo que se quiere lograr
|
||||
- `dependencias` (opcional): issues de los que depende
|
||||
|
||||
### 3. Generar slug
|
||||
|
||||
Título → lowercase → palabras separadas por guiones → 2-4 palabras
|
||||
|
||||
### 4. Evaluar tamaño
|
||||
|
||||
**Criterios para sub-issues:**
|
||||
- Toca más de 2 capas (core/ + shell/ + app/)
|
||||
- Requiere más de 3 fases
|
||||
- El usuario lo indica
|
||||
|
||||
**Issue simple:** crear un archivo `dev/issues/<NNNN>-<slug>.md`
|
||||
|
||||
**Issue grande:** crear SOLO sub-issues `<NNNN>a-`, `<NNNN>b-`, etc.
|
||||
|
||||
### 5. Crear desde template
|
||||
|
||||
Usar template en `${CLAUDE_SKILL_DIR}/issue.md` y rellenar todas las secciones:
|
||||
- Metadata, Objetivo, Contexto
|
||||
- Arquitectura, Patrón pure/impure
|
||||
- Tareas, Ejemplo de uso
|
||||
- Criterios de aceptación
|
||||
|
||||
### 6. Feature flag (solo multi-issue)
|
||||
|
||||
Actualizar `dev/feature_flags.json`:
|
||||
```json
|
||||
{
|
||||
"<nombre-flag>": {
|
||||
"enabled": false,
|
||||
"issue": "<NNNN>",
|
||||
"description": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Actualizar índice
|
||||
|
||||
En `dev/issues/README.md` agregar fila(s).
|
||||
|
||||
### 8. Mostrar y confirmar
|
||||
|
||||
```
|
||||
Issue creado: <NNNN>-<slug>
|
||||
|
||||
¿Te parece bien?
|
||||
- Si es correcto: commit y push automáticamente
|
||||
- Si necesitas ajustes: edita manualmente
|
||||
```
|
||||
|
||||
### 9. Ejecutar /git-push automáticamente
|
||||
|
||||
Si confirma, crear rama `quick/create-issue-<NNNN>` y ejecutar flujo git.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Numeración continua sin saltos
|
||||
- Estado inicial: pendiente
|
||||
- Issues cortos (horas por rama)
|
||||
- Sub-issues autocontenidos
|
||||
@@ -1,128 +0,0 @@
|
||||
# NNNN — [Título de la Issue]
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | NNNN |
|
||||
| **Estado** | 🟡 pendiente / 🔵 en progreso / ✅ completado / 🔴 bloqueado |
|
||||
| **Prioridad** | alta / media / baja |
|
||||
| **Tipo** | feature / bugfix / refactor / docs / infrastructure |
|
||||
|
||||
## Dependencias
|
||||
|
||||
<!-- Issues que DEBEN estar completadas antes de empezar esta -->
|
||||
|
||||
| ID | Título | Estado | Requerido |
|
||||
|----|--------|--------|-----------|
|
||||
| 0001 | Actualizar nombre del módulo | ✅ | Sí |
|
||||
| 0002 | Implementar core/ | ✅ | Sí |
|
||||
|
||||
**Bloqueada por:** `#0001, #0002`
|
||||
|
||||
**Desbloquea:** `#0006, #0007`
|
||||
|
||||
> **⚠️ VALIDACIÓN AUTOMÁTICA**: Esta issue no puede iniciarse hasta que todas las dependencias estén en estado `✅ completado`.
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
[Descripción concisa de qué se quiere lograr en 1-3 oraciones]
|
||||
|
||||
## Contexto
|
||||
|
||||
- [Punto de contexto 1]
|
||||
- [Punto de contexto 2]
|
||||
- [Referencias a otras issues o decisiones previas]
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
[Estructura de archivos afectados]
|
||||
dir/
|
||||
├── file1.go — Descripción
|
||||
├── file2.go — NEW: Nuevo archivo
|
||||
└── file3.go — MODIFY: Modificación
|
||||
```
|
||||
|
||||
### Patrón pure core / impure shell
|
||||
|
||||
- `core/` — [Qué funciones puras se agregan]
|
||||
- `shell/` — [Qué operaciones I/O se implementan]
|
||||
- `app/` — [Cómo se orquesta]
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1: [Nombre de fase]
|
||||
|
||||
- [ ] **1.1** [Descripción detallada de tarea]
|
||||
- [ ] **1.2** [Otra tarea]
|
||||
|
||||
### Fase 2: [Otra fase]
|
||||
|
||||
- [ ] **2.1** [Tarea]
|
||||
- [ ] **2.2** [Tarea]
|
||||
|
||||
### Fase N: Cleanup y docs
|
||||
|
||||
- [ ] Actualizar `README.md` con cambios relevantes
|
||||
- [ ] Actualizar `CLAUDE.md` si hay cambios arquitectónicos
|
||||
- [ ] Ejecutar `go mod tidy`
|
||||
- [ ] Ejecutar `go test ./...`
|
||||
- [ ] Actualizar issue en `dev/issues/README.md`
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo de uso
|
||||
|
||||
```bash
|
||||
# Comandos de ejemplo
|
||||
comando ejemplo arg1 arg2
|
||||
|
||||
# Output esperado:
|
||||
# ✓ Success message
|
||||
```
|
||||
|
||||
```go
|
||||
// Código de ejemplo si aplica
|
||||
package example
|
||||
|
||||
func Example() {}
|
||||
```
|
||||
|
||||
## Decisiones de diseño
|
||||
|
||||
- **Decisión 1**: Razón y trade-offs
|
||||
- **Decisión 2**: Alternativas consideradas y por qué se eligió esta
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
- Issue #NNNN completado
|
||||
- Herramienta X instalada
|
||||
- Configuración Y realizada
|
||||
|
||||
## Riesgos
|
||||
|
||||
- **Riesgo 1**: Descripción del riesgo. **Mitigación**: Cómo se mitigará
|
||||
- **Riesgo 2**: Otro riesgo. **Mitigación**: Plan de mitigación
|
||||
|
||||
## Criterios de aceptación
|
||||
|
||||
- [ ] Todos los tests pasan
|
||||
- [ ] Feature flag agregado en `feature_flags.json`
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Deployable a main
|
||||
|
||||
---
|
||||
|
||||
## Notas de implementación
|
||||
|
||||
[Notas que surjan durante la implementación, decisiones tomadas, problemas encontrados]
|
||||
|
||||
## Referencias
|
||||
|
||||
- [Link a documentación relevante]
|
||||
- [Link a PRs relacionados]
|
||||
- [Link a discusiones]
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: create-repo
|
||||
description: Crea un nuevo subrepo en workspaces/ con estructura core/shell/app
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write
|
||||
---
|
||||
|
||||
# create-repo
|
||||
|
||||
Crea un nuevo workspace (subrepo) con estructura estándar, repo en Gitea, y registro en BD.
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
- Variables: `GITEA_URL` y `GITEA_TOKEN`
|
||||
- Feature flag `workspace_commands` habilitado
|
||||
|
||||
## Flujo interactivo
|
||||
|
||||
### 1. Solicitar inputs
|
||||
|
||||
1. **Nombre**: URL-safe (lowercase, alfanumérico, guiones)
|
||||
2. **Descripción**: texto libre
|
||||
3. **Tipo**: go, data, etl, api
|
||||
4. **¿Privado?**: s/n (default: n)
|
||||
|
||||
### 2. Mostrar resumen y confirmar
|
||||
|
||||
```
|
||||
Resumen:
|
||||
Nombre: my-etl-pipeline
|
||||
Path local: ./workspaces/my-etl-pipeline
|
||||
Gitea: https://gitea.../my-etl-pipeline
|
||||
Tipo: etl
|
||||
Privado: no
|
||||
|
||||
¿Crear repositorio? (s/n):
|
||||
```
|
||||
|
||||
### 3. Ejecutar creación
|
||||
|
||||
Usa `app.CreateWorkspaceCommand(config, params)`:
|
||||
1. Validar nombre
|
||||
2. Verificar que no existe
|
||||
3. Crear estructura core/shell/app/
|
||||
4. Escribir templates (go.mod, main.go, etc.)
|
||||
5. git init + configurar usuario
|
||||
6. Crear repo en Gitea
|
||||
7. Push inicial
|
||||
8. Registrar en SQLite
|
||||
|
||||
**Rollback automático** si falla cualquier paso.
|
||||
|
||||
### 4. Mostrar resultado
|
||||
|
||||
```
|
||||
Workspace creado: ./workspaces/my-etl-pipeline
|
||||
|
||||
Para trabajar:
|
||||
cd workspaces/my-etl-pipeline
|
||||
```
|
||||
|
||||
## Validación de nombre
|
||||
|
||||
- Solo letras, números y guiones
|
||||
- No empezar/terminar con guión
|
||||
- 2-100 caracteres
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- "nombre inválido": usar solo lowercase, alfanumérico, guiones
|
||||
- "ya existe": verificar `ls workspaces/` o usar otro nombre
|
||||
- "error Gitea": verificar GITEA_TOKEN
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: create-tui
|
||||
description: Scaffoldea una aplicación TUI en Go usando DevFactory (bubbletea) para gestionar scripts, comandos, Makefile y builds de un repositorio
|
||||
argument-hint: [nombre] [--path /ruta/destino]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# create-tui
|
||||
|
||||
Genera un proyecto TUI completo en Go usando los componentes de DevFactory (`tui/` — bubbletea, lipgloss). El TUI resultante permite gestionar un repositorio: ejecutar scripts bash, comandos frecuentes, targets de Makefile y configuraciones de build.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/create-tui [nombre] [--path /ruta/destino]
|
||||
```
|
||||
|
||||
- `nombre`: nombre del proyecto (kebab-case). Si no se da, se pregunta.
|
||||
- `--path`: directorio destino. Default: directorio actual.
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Ejecutar script de setup
|
||||
|
||||
```bash
|
||||
bash "${CLAUDE_SKILL_DIR}/setup-create-tui.sh" [nombre] [path]
|
||||
```
|
||||
|
||||
### 2. Si el script reporta STATUS: CONFIGURED
|
||||
|
||||
Informar al usuario que el proyecto TUI ya existe en esa ruta.
|
||||
|
||||
### 3. Si el script reporta STATUS: READY
|
||||
|
||||
Mostrar resumen:
|
||||
- Estructura creada (app/, views/, config/)
|
||||
- Cómo ejecutar: `make run` o `go run .`
|
||||
- Cómo compilar: `make build`
|
||||
- Cómo instalar: `make install`
|
||||
- Navegación: flechas para moverse, Enter para interactuar, Esc/0 para volver, Esc desde menú principal para salir
|
||||
|
||||
### 4. Si el script reporta STATUS: ERROR
|
||||
|
||||
Mostrar el error y sugerir corrección.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Usa DevFactory como dependencia via `go.work` (componentes tui/, shell/, core/)
|
||||
- Patrón Elm Architecture de bubbletea (Model → Update → View)
|
||||
- `Result[T]` del core de DevFactory para manejo de errores
|
||||
- Ejecución async de comandos via `tea.Cmd`
|
||||
- Navegación: flechas + Enter + Esc/0 en todas las vistas
|
||||
- El TUI opera sobre un directorio target (default: `.`, configurable por argumento)
|
||||
+1486
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: execute-parallel
|
||||
description: Ejecuta automáticamente issues del plan de ejecución paralela
|
||||
argument-hint: [--group N] [--sequential]
|
||||
argument-hint: [--group N] [--sequential] [--sort] [--dry-run] [--cleanup]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write
|
||||
@@ -9,7 +9,7 @@ allowed-tools: Bash, Read, Write
|
||||
|
||||
# execute-parallel
|
||||
|
||||
Ejecuta automáticamente las issues del plan paralelo. Crea worktrees, ejecuta /fix-issue, mergea y limpia.
|
||||
Ejecuta automáticamente issues en paralelo usando git worktrees. Unifica sort, plan y ejecución.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
@@ -17,75 +17,92 @@ Ejecuta automáticamente las issues del plan paralelo. Crea worktrees, ejecuta /
|
||||
/execute-parallel # Ejecutar TODOS los grupos
|
||||
/execute-parallel --group 1 # Solo Grupo 1
|
||||
/execute-parallel --sequential # Sin paralelismo
|
||||
/execute-parallel --dry-run # Ver plan sin ejecutar
|
||||
/execute-parallel --sort # Solo analizar y generar plan
|
||||
/execute-parallel --cleanup # Solo limpiar worktrees
|
||||
```
|
||||
|
||||
## Binario
|
||||
|
||||
El orquestador está en `utils/parallel-executor/` y se compila a `bin/parallel-executor`.
|
||||
|
||||
```bash
|
||||
# Si no existe el binario, compilar primero
|
||||
if [ ! -f "bin/parallel-executor" ]; then
|
||||
cd utils/parallel-executor && make build && cd ../..
|
||||
fi
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Validar precondiciones
|
||||
### 1. Verificar binario
|
||||
|
||||
```bash
|
||||
# Si no existe plan, generarlo automáticamente
|
||||
if [ ! -f "PARALLEL_EXECUTION_ORDER.md" ]; then
|
||||
/parallel-issues
|
||||
EXECUTOR="./bin/parallel-executor"
|
||||
if [ ! -f "$EXECUTOR" ]; then
|
||||
echo "Compilando parallel-executor..."
|
||||
cd utils/parallel-executor && make build && cd ../..
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. Parsear argumentos
|
||||
### 2. Parsear argumentos del usuario y ejecutar
|
||||
|
||||
- `--group <N>`: ejecutar solo ese grupo
|
||||
- `--sequential`: ejecutar uno a uno
|
||||
- Sin args: ejecutar todos los grupos
|
||||
|
||||
### 3. Ejecutar programa Go
|
||||
Mapear los argumentos directamente al binario:
|
||||
|
||||
```bash
|
||||
./cmd/parallel-executor/parallel-executor $ARGS
|
||||
./bin/parallel-executor $ARGS
|
||||
```
|
||||
|
||||
El orquestador Go maneja:
|
||||
- Creación de worktrees
|
||||
- Ejecución paralela de `/fix-issue`
|
||||
- Push de cada rama
|
||||
- Limpieza de worktrees
|
||||
- Logging en `logs/`
|
||||
**Flags disponibles:**
|
||||
- `--sort` → analizar issues y generar PARALLEL_EXECUTION_ORDER.md
|
||||
- `--dry-run` → mostrar plan y worktrees que se crearían
|
||||
- `--group N` → ejecutar solo grupo N
|
||||
- `--sequential` → ejecutar sin paralelismo
|
||||
- `--timeout N` → timeout en minutos por issue (default: 30)
|
||||
- `--cleanup` → solo limpiar worktrees existentes
|
||||
- `--plan file` → usar plan alternativo
|
||||
|
||||
### 4. Mostrar resumen
|
||||
### 3. Mostrar resumen
|
||||
|
||||
Después de ejecutar, mostrar:
|
||||
- Resultados por issue (éxito/fallo)
|
||||
- Ruta a logs y summary
|
||||
- Estado de worktrees
|
||||
|
||||
### 4. Limpiar plan si exitoso
|
||||
|
||||
Si todos los issues completaron exitosamente, eliminar `PARALLEL_EXECUTION_ORDER.md`.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
Ejecución completada
|
||||
|
||||
Logs:
|
||||
- logs/parallel-execution-*.log
|
||||
- logs/consolidated-summary.txt
|
||||
|
||||
Worktrees restantes: (ninguno)
|
||||
```
|
||||
|
||||
### 5. Eliminar plan
|
||||
|
||||
Si exitoso, eliminar `PARALLEL_EXECUTION_ORDER.md`.
|
||||
|
||||
## Arquitectura Go
|
||||
|
||||
```
|
||||
cmd/parallel-executor/
|
||||
├── main.go # CLI
|
||||
├── parser.go # Parse plan
|
||||
├── worktree.go # Git worktrees
|
||||
├── executor.go # Ejecutar claude
|
||||
├── logger.go # Logging
|
||||
└── orchestrator.go # Goroutines
|
||||
utils/parallel-executor/
|
||||
├── main.go # CLI + orquestación
|
||||
├── core/
|
||||
│ ├── parser.go # Parseo del plan markdown (puro)
|
||||
│ ├── planner.go # Topological sort + conflictos (puro)
|
||||
│ ├── parser_test.go
|
||||
│ └── planner_test.go
|
||||
├── shell/
|
||||
│ ├── worktree.go # Git worktree CRUD
|
||||
│ ├── executor.go # Ejecutar claude en worktree
|
||||
│ └── logger.go # Logging a disco
|
||||
├── go.mod, go.work # DevFactory como dependencia
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Logs persistentes por ejecución
|
||||
- Timeout 30 min por issue
|
||||
- Limpieza automática de worktrees
|
||||
- Plan se elimina al completar
|
||||
- Usa DevFactory: `Result[T]`, `MapSlice`, `FilterSlice`, `Reduce`
|
||||
- Patrón pure core / impure shell
|
||||
- Logs persistentes en `logs/`
|
||||
- Timeout 30 min por issue (configurable)
|
||||
- Limpieza automática de worktrees al terminar
|
||||
- Plan se auto-genera si no existe (`--sort` implícito)
|
||||
|
||||
## Reglas
|
||||
|
||||
- SIEMPRE generar plan si no existe
|
||||
- Solo advertir si hay cambios (no bloquear)
|
||||
- SIEMPRE verificar que el binario existe antes de ejecutar
|
||||
- SIEMPRE mostrar dry-run antes de una ejecución real si el usuario no especificó flags
|
||||
- SIEMPRE limpiar worktrees al terminar
|
||||
- Si no hay plan, generarlo automáticamente
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
---
|
||||
name: fix-issue
|
||||
description: Implementa un issue completo de punta a punta con confirmación
|
||||
argument-hint: <NNNN>
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit, TodoWrite
|
||||
---
|
||||
|
||||
# fix-issue
|
||||
|
||||
Ejecuta el flujo completo de implementación/cierre de un issue: crear rama, implementar, testear, cerrar, confirmar, integrar.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/fix-issue <NNNN>
|
||||
/fix-issue <NNNN>-<slug>
|
||||
```
|
||||
|
||||
## Precondiciones
|
||||
|
||||
- [ ] Directorio `dev/issues/` existe
|
||||
- [ ] Directorio `dev/issues/completed/` existe
|
||||
- [ ] Tests configurados
|
||||
- [ ] Working tree limpio
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Resolver issue objetivo
|
||||
|
||||
```bash
|
||||
ls dev/issues/<NNNN>-*.md
|
||||
```
|
||||
|
||||
- Si no existe: STOP "Issue no encontrado"
|
||||
- Si ya completado: STOP "Issue ya completado"
|
||||
|
||||
### 2. Leer issue completo
|
||||
|
||||
Extraer: objetivo, tareas, arquitectura, patrón pure/impure, tests.
|
||||
|
||||
### 3. Crear rama de trabajo
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
git checkout -b issue/<NNNN>-<slug>
|
||||
```
|
||||
|
||||
### 4. Planificar con TodoWrite
|
||||
|
||||
Crear plan basado en tareas del issue.
|
||||
|
||||
### 5. Implementar completo
|
||||
|
||||
Para cada tarea:
|
||||
1. Implementar siguiendo patrón pure core / impure shell
|
||||
2. Compilar frecuentemente: `go build -tags goolm ./...`
|
||||
3. Crear commits atómicos durante implementación
|
||||
|
||||
### 6. Tests obligatorios
|
||||
|
||||
```bash
|
||||
go test -tags goolm ./...
|
||||
```
|
||||
|
||||
- Pasan: continuar
|
||||
- Fallan: STOP y corregir
|
||||
|
||||
### 7. Feature flags (si aplica)
|
||||
|
||||
Actualizar `dev/feature_flags.json` si es multi-issue.
|
||||
|
||||
### 8. Cerrar issue
|
||||
|
||||
```bash
|
||||
mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/
|
||||
```
|
||||
|
||||
Actualizar índice en README.md.
|
||||
|
||||
### 9. Mostrar resumen y confirmar
|
||||
|
||||
```
|
||||
Issue <NNNN> completado
|
||||
|
||||
Resumen:
|
||||
- N archivos modificados
|
||||
- N commits realizados
|
||||
- Tests: pasando
|
||||
|
||||
¿Integrar a master?
|
||||
```
|
||||
|
||||
### 10. Ejecutar /git-push
|
||||
|
||||
Si confirma, ejecutar flujo de integración.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Implementar TODAS las tareas
|
||||
- Commits atómicos durante implementación
|
||||
- Tests obligatorios
|
||||
- Pure core / impure shell
|
||||
|
||||
## Reglas
|
||||
|
||||
- NO saltear tareas
|
||||
- NO commits WIP
|
||||
- SIEMPRE tests antes de cerrar
|
||||
- Confirmación obligatoria antes de integrar
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
name: git-branch
|
||||
description: Crea una rama de trabajo (issue/* o quick/*). Nunca trabajar directamente en master.
|
||||
argument-hint: <tipo> <args>
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# git-branch
|
||||
|
||||
Crea una rama de trabajo siguiendo trunk-based development. **Nunca trabajar directamente en master.**
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/git-branch issue <NNNN> <slug>
|
||||
/git-branch quick <slug>
|
||||
```
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```bash
|
||||
/git-branch issue 0013 hot-reload # Crea issue/0013-hot-reload
|
||||
/git-branch quick fix-typo-readme # Crea quick/fix-typo-readme
|
||||
```
|
||||
|
||||
## Precondiciones
|
||||
|
||||
- [ ] Repositorio git válido
|
||||
- [ ] Branch master existe
|
||||
- [ ] Working tree limpio (sin cambios pendientes)
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Verificar estado del repositorio
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --short
|
||||
```
|
||||
|
||||
**Si no estamos en master:** `git checkout master`
|
||||
|
||||
**Si hay cambios sin commitear:** STOP y avisar al usuario:
|
||||
```
|
||||
Hay cambios sin commitear. Opciones:
|
||||
1. Commitear: git add . && git commit -m "mensaje"
|
||||
2. Stash: git stash
|
||||
3. Descartar: git reset --hard (peligroso)
|
||||
```
|
||||
|
||||
### 2. Actualizar master desde remoto
|
||||
|
||||
```bash
|
||||
git pull --rebase
|
||||
```
|
||||
|
||||
### 3. Crear rama según tipo
|
||||
|
||||
**Para issues:**
|
||||
```bash
|
||||
git checkout -b issue/<NNNN>-<slug>
|
||||
```
|
||||
|
||||
**Para cambios rápidos:**
|
||||
```bash
|
||||
git checkout -b quick/<slug>
|
||||
```
|
||||
|
||||
### 4. Confirmar creación
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Informar:
|
||||
```
|
||||
Rama `<nombre-rama>` creada desde master actualizado
|
||||
|
||||
Cuando termines:
|
||||
/git-push
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- **Formato issue**: `issue/<NNNN>-<slug>` (4 dígitos)
|
||||
- **Formato quick**: `quick/<slug>`
|
||||
- **Ramas cortas**: horas, no días
|
||||
- **No pushear ramas**: integrar via merge a master
|
||||
- **No underscores**: solo guiones
|
||||
|
||||
## Reglas
|
||||
|
||||
- NUNCA trabajar directamente en master
|
||||
- SIEMPRE verificar working tree limpio
|
||||
- SIEMPRE actualizar master antes de crear rama
|
||||
@@ -1,116 +0,0 @@
|
||||
---
|
||||
name: git-push
|
||||
description: Integra cambios a master y publica. Soporta ramas issue/* y quick/*.
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# git-push
|
||||
|
||||
Integra cambios a master y publica al remoto. Detecta automáticamente la rama actual.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/git-push
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Verificar rama actual y estado
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --short
|
||||
```
|
||||
|
||||
**Caso A: En rama issue/* o quick/*** - Continuar al paso 2
|
||||
|
||||
**Caso B: En master con cambios** - Crear rama quick automáticamente:
|
||||
- Analizar archivos modificados para generar slug
|
||||
- `git checkout -b quick/<slug-generado>`
|
||||
|
||||
**Caso C: En master sin cambios** - STOP: "No hay nada que publicar"
|
||||
|
||||
### 2. Crear commits por bloque lógico
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git diff --stat
|
||||
```
|
||||
|
||||
Agrupar cambios por tipo y crear commits atómicos:
|
||||
|
||||
```bash
|
||||
git add <archivos_bloque_1>
|
||||
git commit -m "<tipo>: <resumen>" -m "<descripción en español>"
|
||||
```
|
||||
|
||||
**Tipos:** feat, fix, refactor, docs, chore, test
|
||||
|
||||
**Reglas de commits:**
|
||||
- No WIP
|
||||
- No mezclar tipos
|
||||
- Descripción larga obligatoria en español
|
||||
|
||||
### 3. Ejecutar tests
|
||||
|
||||
```bash
|
||||
go test -tags goolm ./...
|
||||
```
|
||||
|
||||
- Tests pasan: continuar
|
||||
- Tests fallan: STOP y corregir
|
||||
- No hay tests: informar y continuar
|
||||
|
||||
### 4. Merge a master
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
git merge --no-ff <rama> -m "merge: <rama> — <título>"
|
||||
```
|
||||
|
||||
### 5. Push a remoto
|
||||
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
### 6. Limpiar rama local
|
||||
|
||||
```bash
|
||||
git branch -d <rama>
|
||||
```
|
||||
|
||||
### 7. Verificación final
|
||||
|
||||
```bash
|
||||
git log --oneline -3
|
||||
```
|
||||
|
||||
```
|
||||
Rama `<rama>` integrada a master y publicada
|
||||
|
||||
Commits creados:
|
||||
- <commit 1>
|
||||
- merge: <rama>
|
||||
|
||||
Rama local eliminada.
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Commits atómicos
|
||||
- Tests obligatorios antes de merge
|
||||
- Merge --no-ff siempre
|
||||
- Push inmediato
|
||||
|
||||
## Reglas
|
||||
|
||||
- NO commits WIP
|
||||
- NO mezclar tipos en un commit
|
||||
- NO saltear tests
|
||||
- NO push --force a master
|
||||
- SIEMPRE usar --no-ff
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
name: git-recovery
|
||||
description: Recupera el repositorio de estados inconsistentes (worktrees huérfanos, branches bloqueados)
|
||||
argument-hint: [--aggressive]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# git-recovery
|
||||
|
||||
Recupera el repositorio de estados inconsistentes causados por worktrees huérfanos, branches bloqueados o conflictos git.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/git-recovery # Recuperación estándar
|
||||
/git-recovery --aggressive # Limpieza agresiva
|
||||
```
|
||||
|
||||
## Cuándo usar
|
||||
|
||||
- Errores "exit status 128" al crear worktrees
|
||||
- Git reporta "worktree already exists"
|
||||
- Branches que no se pueden eliminar
|
||||
- Worktrees huérfanos en `git worktree list`
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Diagnóstico inicial
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --porcelain
|
||||
```
|
||||
|
||||
### 2. Análisis de problemas
|
||||
|
||||
```bash
|
||||
git worktree list
|
||||
git branch --list
|
||||
git remote -v
|
||||
```
|
||||
|
||||
### 3. Limpieza de worktrees huérfanos
|
||||
|
||||
```bash
|
||||
git worktree prune -v
|
||||
```
|
||||
|
||||
Si existe directorio `worktrees/`:
|
||||
- Verificar cada worktree contra `git worktree list`
|
||||
- Eliminar directorios huérfanos
|
||||
|
||||
### 4. Verificar branches bloqueados
|
||||
|
||||
Para cada branch issue/* o quick/*:
|
||||
- Si está mergeada: `git branch -d <branch>`
|
||||
- Si NO está mergeada: advertir
|
||||
|
||||
### 5. Sincronizar con remoto
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git fetch origin
|
||||
git pull --rebase origin master
|
||||
```
|
||||
|
||||
### 6. Modo agresivo (solo con --aggressive)
|
||||
|
||||
```bash
|
||||
git remote prune origin -v
|
||||
git fsck --full
|
||||
git gc --prune=now
|
||||
rm -f .git/index.lock # si existe
|
||||
```
|
||||
|
||||
### 7. Verificación final
|
||||
|
||||
```bash
|
||||
git status
|
||||
git worktree list
|
||||
git branch --list
|
||||
```
|
||||
|
||||
## Patrones de error que activan recovery
|
||||
|
||||
- `exit status 128`
|
||||
- `worktree .* already exists`
|
||||
- `reference is not a tree`
|
||||
- `cannot lock ref`
|
||||
- `index.lock`
|
||||
|
||||
## Convenciones
|
||||
|
||||
- No destructivo por defecto
|
||||
- Modo agresivo solo con flag explícito
|
||||
- Siempre sincroniza con remoto
|
||||
- Preserva cambios locales
|
||||
|
||||
## Reglas
|
||||
|
||||
- NUNCA git reset --hard sin --aggressive
|
||||
- NUNCA eliminar branches no mergeadas automáticamente
|
||||
- SIEMPRE sincronizar con remoto después de limpieza
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
name: import-repo
|
||||
description: Importa repositorios existentes al sistema Dataforge desde URL remota o local
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write
|
||||
---
|
||||
|
||||
# import-repo
|
||||
|
||||
Importa repositorios existentes: desde GitHub/GitLab/Gitea, o adoptando un repo local.
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
- Variables: `GITEA_URL` y `GITEA_TOKEN`
|
||||
- Feature flag `workspace_commands` habilitado
|
||||
|
||||
## Modos
|
||||
|
||||
### Desde URL remota
|
||||
|
||||
1. Crear repo vacío en Gitea
|
||||
2. Clonar origen con `git clone --mirror`
|
||||
3. Push a Gitea con `git push --mirror`
|
||||
4. Clonar en `workspaces/`
|
||||
5. Registrar en BD
|
||||
|
||||
### Adoptar repo local
|
||||
|
||||
1. Verificar que existe `.git`
|
||||
2. Crear repo vacío en Gitea
|
||||
3. Añadir remote `gitea`
|
||||
4. Push de branches y tags
|
||||
5. Registrar en BD
|
||||
|
||||
## Flujo interactivo
|
||||
|
||||
### 1. Solicitar fuente
|
||||
|
||||
```
|
||||
Fuente del repositorio:
|
||||
- URL remota (ej: https://github.com/user/repo)
|
||||
- Nombre local en workspaces/ (ej: legacy-tool)
|
||||
```
|
||||
|
||||
### 2. Detectar modo y analizar
|
||||
|
||||
Usa `core.DetectImportMode(source)`
|
||||
|
||||
### 3. Solicitar nombre de destino
|
||||
|
||||
```
|
||||
Nombre en Gitea (Enter para usar 'nombre-sugerido'):
|
||||
```
|
||||
|
||||
### 4. Verificar que no existe en Gitea
|
||||
|
||||
### 5. Opciones adicionales
|
||||
|
||||
```
|
||||
¿Repositorio privado? (s/N):
|
||||
Descripción (opcional):
|
||||
```
|
||||
|
||||
### 6. Resumen y confirmación
|
||||
|
||||
```
|
||||
Resumen:
|
||||
Fuente: https://...
|
||||
Destino: gitea.example.com/...
|
||||
Tipo: importar desde URL remota
|
||||
|
||||
¿Importar? (s/N):
|
||||
```
|
||||
|
||||
### 7. Ejecutar importación
|
||||
|
||||
### 8. Mostrar resultado
|
||||
|
||||
```
|
||||
Repositorio importado exitosamente.
|
||||
|
||||
Workspace: workspaces/nombre
|
||||
Gitea URL: https://...
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Confirmación obligatoria
|
||||
- Rollback automático si falla
|
||||
- Historia Git siempre preservada
|
||||
@@ -19,7 +19,8 @@ Skill para preparar cualquier repo para exploración de datos con Jupyter + Clau
|
||||
|
||||
```bash
|
||||
# Obtener ruta del script (está junto a este SKILL.md)
|
||||
SKILL_DIR="$HOME/DataProyects/repo_Claude/.claude/skills/init-jupyter"
|
||||
# Resolver via symlink a la ubicación real del skill (portable entre máquinas)
|
||||
SKILL_DIR="$(dirname "$(readlink -f "$HOME/.claude/skills/init-jupyter/SKILL.md")")"
|
||||
|
||||
# Ejecutar con la ruta del proyecto (argumento del skill o directorio actual)
|
||||
bash "$SKILL_DIR/setup-jupyter.sh" "${1:-.}"
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
name: issues-status
|
||||
description: Dashboard global de issues en todos los workspaces con métricas y filtros
|
||||
argument-hint: [workspace] [--status pending] [--tag tag] [--export json|csv]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# issues-status
|
||||
|
||||
Muestra dashboard global de todas las issues con métricas, filtros y sugerencias.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/issues-status # Dashboard global
|
||||
/issues-status <workspace> # Detalle de workspace
|
||||
/issues-status --status pending # Filtrar por estado
|
||||
/issues-status --tag backend # Filtrar por tag
|
||||
/issues-status --export json # Exportar
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Parsear argumentos
|
||||
|
||||
- Primer arg (sin --): `filterWorkspace`
|
||||
- `--status <value>`: pending | in_progress | completed
|
||||
- `--tag <value>`: filtrar por tag
|
||||
- `--export <format>`: json | csv
|
||||
|
||||
### 2. Ejecutar dashboard
|
||||
|
||||
Llama `app.IssuesDashboardCommand(config, filterWorkspace, filterStatus, filterTag, exportFormat)`
|
||||
|
||||
### 3. Modo interactivo (dashboard global)
|
||||
|
||||
Si no hay filtros:
|
||||
1. Mostrar dashboard con sugerencias
|
||||
2. Preguntar: "¿Ver detalle de un workspace? (nombre o 'n')"
|
||||
3. Si responde nombre: mostrar detalle
|
||||
4. Si responde 'n': terminar
|
||||
|
||||
### Comandos sugeridos
|
||||
|
||||
```
|
||||
Commands:
|
||||
/issues-status <workspace>
|
||||
/issues-status --status pending
|
||||
/fix-issue <issue>
|
||||
```
|
||||
|
||||
## Manejo de errores
|
||||
|
||||
- Si no hay workspaces: sugerir crear o sincronizar
|
||||
- Si no hay issues: mostrar dashboard vacío con sugerencias
|
||||
@@ -0,0 +1,319 @@
|
||||
---
|
||||
name: parallel-fix-issues
|
||||
description: >
|
||||
Implementar múltiples issues en paralelo. Analiza dependencias entre issues pendientes,
|
||||
crea git worktrees aislados, lanza agentes concurrentes para cada issue, verifica
|
||||
resultados (build + tests) e integra todo a master en orden.
|
||||
allowed-tools: Bash Read Write Edit Grep Glob Agent
|
||||
argument-hint: "[issue-numbers... | all]"
|
||||
---
|
||||
|
||||
# Parallel Fix Issues
|
||||
|
||||
Skill para implementar múltiples issues simultáneamente usando git worktrees y agentes paralelos.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `$ARGUMENTS`: lista de issue numbers (ej: `0026 0027 0031`) o `all` para todos los pendientes.
|
||||
- Si no hay argumentos, preguntar al usuario qué issues quiere procesar.
|
||||
|
||||
## Proceso completo
|
||||
|
||||
### Fase 1: Análisis de dependencias
|
||||
|
||||
Lanzar un **Agent** (subagent_type: `Explore`) para analizar los issues y producir un plan de ejecución.
|
||||
|
||||
El agente debe:
|
||||
|
||||
1. Leer `dev/issues/README.md` y filtrar los issues pendientes
|
||||
2. Si `$ARGUMENTS` no es `all`, filtrar solo los issues solicitados
|
||||
3. Para cada issue pendiente, leer el archivo completo y extraer:
|
||||
- **Objetivo** (resumen)
|
||||
- **Prerequisitos** y dependencias explícitas (ej: "requiere issue 0026")
|
||||
- **Archivos afectados** (para detectar conflictos potenciales entre issues)
|
||||
4. Construir un **grafo de dependencias** y agrupar en **waves** (oleadas):
|
||||
- Wave 1: issues sin dependencias entre sí y sin dependencias pendientes
|
||||
- Wave 2: issues que dependen de wave 1
|
||||
- Wave N: etc.
|
||||
5. Dentro de cada wave, identificar **conflictos potenciales** (dos issues que tocan los mismos archivos)
|
||||
6. Devolver el resultado en este formato exacto:
|
||||
|
||||
```
|
||||
WAVE 1 (paralelo):
|
||||
- <NNNN>-<slug> — <objetivo resumido> — archivos: <lista>
|
||||
- <NNNN>-<slug> — <objetivo resumido> — archivos: <lista>
|
||||
|
||||
WAVE 2 (paralelo, después de wave 1):
|
||||
- <NNNN>-<slug> — <objetivo resumido> — depende de: <NNNN>
|
||||
|
||||
CONFLICTOS POTENCIALES:
|
||||
- <NNNN> y <NNNN> tocan <archivo> — riesgo de merge conflict
|
||||
|
||||
ISSUES EXCLUIDOS:
|
||||
- <NNNN>-<slug> — razón (dependencia externa no resuelta, etc.)
|
||||
```
|
||||
|
||||
**Mostrar el resultado al usuario y pedir confirmación** antes de continuar. El usuario puede:
|
||||
- Aprobar el plan tal cual
|
||||
- Excluir issues específicos
|
||||
- Reordenar waves
|
||||
|
||||
### Fase 2: Setup de worktrees
|
||||
|
||||
Una vez aprobado el plan, crear los worktrees.
|
||||
|
||||
```bash
|
||||
.claude/skills/parallel-fix-issues/scripts/setup-worktrees.sh <slug-1> <slug-2> ...
|
||||
```
|
||||
|
||||
El script crea un worktree por issue en `worktrees/<slug>/`, cada uno en su propia branch `issue/<slug>`.
|
||||
|
||||
**Verificar** que todos los worktrees se crearon correctamente:
|
||||
|
||||
```bash
|
||||
git worktree list
|
||||
```
|
||||
|
||||
### Fase 3: Ejecución paralela por waves
|
||||
|
||||
Para cada wave, lanzar **Agents en paralelo** (un Agent por issue, todos en el mismo mensaje para ejecución concurrente).
|
||||
|
||||
**CRÍTICO**: Lanzar todos los agentes de una wave en una sola respuesta con múltiples tool calls. NO lanzar de uno en uno.
|
||||
|
||||
**PERMISOS**: Todas las llamadas Bash de los agentes deben usar `dangerouslyDisableSandbox: true` para evitar prompts de permisos que bloqueen la ejecución paralela. Indicar esto explícitamente en el prompt de cada agente.
|
||||
|
||||
El prompt de cada agente debe incluir:
|
||||
|
||||
1. **Ruta absoluta del worktree** (calcular con `$(git rev-parse --show-toplevel)/worktrees/<slug>`, o pasar la ruta literal ya resuelta)
|
||||
2. **Build tag Go** del proyecto (detectar — ver "Detección del build tag" más abajo)
|
||||
3. **Contenido completo del issue** (copiar el markdown entero)
|
||||
4. **Instrucciones de ejecución** (ver template abajo)
|
||||
|
||||
#### Detección del stack y comandos build/test
|
||||
|
||||
Antes de lanzar los agentes, detectar el stack del proyecto y los comandos correspondientes. La skill es **agnostica del lenguaje**: soporta Go, C++, Rust, Node, Python o cualquier otro stack via override.
|
||||
|
||||
**Resolucion de comandos** (en orden de prioridad):
|
||||
|
||||
1. **Override explicito** del usuario (env vars `BUILD_CMD` y `TEST_CMD` o argumentos al invocar la skill).
|
||||
2. **Manifest opcional** `.parallel-fix-issues.yml` en la raiz del repo:
|
||||
```yaml
|
||||
build: "cmake -S cpp -B cpp/build && cmake --build cpp/build -j"
|
||||
test: "ctest --test-dir cpp/build --output-on-failure"
|
||||
```
|
||||
3. **Auto-deteccion** segun ficheros raiz:
|
||||
- `go.mod` → `go build [-tags X] ./...` + `go test [-tags X] ./...` (X auto-detectado de `//go:build`)
|
||||
- `CMakeLists.txt` (raiz o `cpp/`) → `cmake -S <dir> -B <dir>/build -DCMAKE_BUILD_TYPE=Release && cmake --build <dir>/build -j` + `ctest --test-dir <dir>/build --output-on-failure || true`
|
||||
- `Cargo.toml` → `cargo build` + `cargo test`
|
||||
- `package.json` → `npm run build --if-present` + `npm test --if-present`
|
||||
- `pyproject.toml` / `setup.py` → (sin build) + `pytest`
|
||||
4. Si nada se detecta, **preguntar al usuario** que comandos usar antes de continuar.
|
||||
|
||||
**Mostrar al usuario los comandos resueltos** y pedir confirmacion antes de seguir. Pasar tanto `BUILD_CMD` como `TEST_CMD` (ya resueltos) al prompt de cada agente.
|
||||
|
||||
#### Template de prompt para cada agente
|
||||
|
||||
```
|
||||
Eres un agente de desarrollo implementando el issue <NNNN>-<slug>.
|
||||
|
||||
## Directorio de trabajo
|
||||
|
||||
Worktree: <RUTA_ABSOLUTA_DEL_WORKTREE> # ej: /home/user/proyecto/worktrees/<slug>
|
||||
|
||||
Usa SIEMPRE esta ruta como prefijo en paths absolutos.
|
||||
Variable de conveniencia para comandos:
|
||||
W=<RUTA_ABSOLUTA_DEL_WORKTREE>
|
||||
|
||||
## Comandos build/test del proyecto
|
||||
|
||||
BUILD_CMD=<COMANDO_RESUELTO> # ej: "cmake -S cpp -B cpp/build && cmake --build cpp/build -j"
|
||||
TEST_CMD=<COMANDO_RESUELTO> # ej: "ctest --test-dir cpp/build --output-on-failure"
|
||||
|
||||
Estos comandos ya estan resueltos por el orquestador (auto-deteccion, override o manifest
|
||||
.parallel-fix-issues.yml). Usalos tal cual desde la raiz del worktree:
|
||||
|
||||
Bash({ command: "cd $W && eval \"$BUILD_CMD\"", dangerouslyDisableSandbox: true })
|
||||
Bash({ command: "cd $W && eval \"$TEST_CMD\"", dangerouslyDisableSandbox: true })
|
||||
|
||||
Si el issue requiere comandos adicionales (ej. `./fn index` tras añadir funciones, `npm install`,
|
||||
`uv sync`), ejecutalos antes/despues segun corresponda.
|
||||
|
||||
## Permisos
|
||||
|
||||
IMPORTANTE: En TODAS tus llamadas al tool Bash, usa el parámetro `dangerouslyDisableSandbox: true`.
|
||||
Esto es necesario porque estás ejecutando en paralelo con otros agentes y no hay usuario interactivo
|
||||
para aprobar permisos. Ejemplo:
|
||||
|
||||
Bash({ command: "cd $W && $GO_BUILD", dangerouslyDisableSandbox: true })
|
||||
|
||||
## Issue a implementar
|
||||
|
||||
<PEGAR CONTENIDO COMPLETO DEL ISSUE AQUÍ>
|
||||
|
||||
## Instrucciones
|
||||
|
||||
Sigue este flujo estrictamente:
|
||||
|
||||
1. **Leer el issue** — ya lo tienes arriba, entiende objetivo, tareas y arquitectura.
|
||||
|
||||
2. **Implementar todas las tareas** en orden:
|
||||
- Respetar las convenciones del proyecto (pure core / impure shell si aplica)
|
||||
- Hacer commits atomicos por bloque logico
|
||||
- Prefijos: feat:, fix:, test:, docs:, refactor:, chore:
|
||||
- NO hacer commits WIP ni codigo a medias
|
||||
- Compilar frecuentemente:
|
||||
Bash({ command: "cd $W && eval \"$BUILD_CMD\"", dangerouslyDisableSandbox: true })
|
||||
|
||||
3. **Tests obligatorios** (en el lenguaje/framework apropiado del stack):
|
||||
- Escribir tests para todo codigo nuevo. Usar el framework convencional del lenguaje:
|
||||
Go → testing pkg, C++ → ctest/Catch2/gtest, Rust → cargo test, Python → pytest, etc.
|
||||
- Ejecutar:
|
||||
Bash({ command: "cd $W && eval \"$TEST_CMD\"", dangerouslyDisableSandbox: true })
|
||||
- NO continuar si los tests fallan
|
||||
- Si el issue requiere paso de indexacion u otros (ej. `./fn index`, `npm install`), ejecutarlo aqui
|
||||
|
||||
4. **Cerrar el issue** — solo mover el archivo, NO tocar README:
|
||||
- Bash({ command: "cd $W && git mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/", dangerouslyDisableSandbox: true })
|
||||
- Commit: docs: cerrar issue <NNNN>
|
||||
IMPORTANTE: usar `git mv` (no `mv` + `git add`) para que git registre el movimiento.
|
||||
IMPORTANTE: NO modificar dev/issues/README.md — lo hace el orquestador después del merge
|
||||
para evitar conflictos entre agentes paralelos.
|
||||
|
||||
5. **NO hacer merge a master, NO hacer push.** La integración la maneja el orquestador.
|
||||
|
||||
6. **Reportar resultado** al final:
|
||||
- ÉXITO: qué se implementó, cuántos commits, tests pasando
|
||||
- FALLO: qué falló, en qué paso, qué queda pendiente
|
||||
```
|
||||
|
||||
**Esperar** a que todos los agentes de la wave terminen antes de pasar a la siguiente wave.
|
||||
|
||||
### Fase 4: Verificación
|
||||
|
||||
Después de cada wave, verificar TODOS los worktrees completados:
|
||||
|
||||
```bash
|
||||
.claude/skills/parallel-fix-issues/scripts/verify-worktree.sh worktrees/<slug>
|
||||
```
|
||||
|
||||
El script verifica:
|
||||
- `$BUILD_CMD` — compila sin errores (auto-detectado o pasado por env/arg)
|
||||
- `$TEST_CMD` — tests pasan
|
||||
- Issue movido a `dev/issues/completed/`
|
||||
- Al menos 1 commit en la branch
|
||||
|
||||
Pasar `BUILD_CMD` y `TEST_CMD` como variables de entorno o argumentos posicionales:
|
||||
|
||||
```bash
|
||||
BUILD_CMD="cmake --build cpp/build" TEST_CMD="ctest --test-dir cpp/build" \
|
||||
.claude/skills/parallel-fix-issues/scripts/verify-worktree.sh worktrees/<slug>
|
||||
# o posicionales
|
||||
.claude/skills/parallel-fix-issues/scripts/verify-worktree.sh worktrees/<slug> "go build ./..." "go test ./..."
|
||||
```
|
||||
|
||||
Si no se pasan, el script auto-detecta el stack (go.mod, CMakeLists.txt, Cargo.toml, package.json, pyproject.toml).
|
||||
|
||||
**Si un worktree falla verificación**:
|
||||
1. Reportar al usuario qué falló
|
||||
2. Preguntar si quiere: (a) intentar arreglar, (b) excluir ese issue, (c) abortar todo
|
||||
3. Si se excluye, marcar para no integrar
|
||||
|
||||
### Fase 5: Integración a master
|
||||
|
||||
Una vez todas las waves verificadas, integrar a master **en orden de waves** (wave 1 primero, luego wave 2, etc.).
|
||||
|
||||
```bash
|
||||
.claude/skills/parallel-fix-issues/scripts/integrate-worktrees.sh <slug-1> <slug-2> ...
|
||||
```
|
||||
|
||||
El script hace para cada branch:
|
||||
1. `git checkout master`
|
||||
2. `git merge --no-ff issue/<slug>` con mensaje descriptivo
|
||||
3. Si hay **merge conflict**: PARAR e informar al usuario
|
||||
|
||||
**Despues de cada merge**, re-verificar que master compila usando los `BUILD_CMD`/`TEST_CMD` resueltos:
|
||||
|
||||
```bash
|
||||
eval "$BUILD_CMD" && eval "$TEST_CMD"
|
||||
```
|
||||
|
||||
`integrate-worktrees.sh` ya verifica el build post-merge si `BUILD_CMD` esta exportado.
|
||||
Si falla despues de un merge, PARAR e informar — no continuar con mas merges.
|
||||
|
||||
### Fase 6: Actualizar README de issues
|
||||
|
||||
Después de integrar TODOS los issues exitosos, actualizar `dev/issues/README.md` **una sola vez** desde master.
|
||||
Esto evita conflictos: los agentes paralelos solo mueven archivos, el orquestador actualiza el índice.
|
||||
|
||||
Para cada issue integrado:
|
||||
1. Cambiar el link de `[<NNNN>-<slug>.md](<NNNN>-<slug>.md)` a `[<NNNN>-<slug>.md](completed/<NNNN>-<slug>.md)`
|
||||
2. Cambiar el estado de `pendiente` a `completado`
|
||||
|
||||
Hacer un solo commit:
|
||||
|
||||
```bash
|
||||
git add dev/issues/README.md
|
||||
git commit -m "docs: actualizar README de issues — marcar <N> issues como completados"
|
||||
```
|
||||
|
||||
### Fase 7: Limpieza
|
||||
|
||||
Si todo fue exitoso:
|
||||
|
||||
```bash
|
||||
# Eliminar worktrees y branches
|
||||
for slug in <slugs...>; do
|
||||
git worktree remove "worktrees/${slug}" 2>/dev/null
|
||||
git branch -d "issue/${slug}" 2>/dev/null
|
||||
done
|
||||
```
|
||||
|
||||
### Fase 8: Reporte final
|
||||
|
||||
Mostrar al usuario un resumen:
|
||||
|
||||
```
|
||||
## Resultado de parallel-fix-issues
|
||||
|
||||
### Issues completados
|
||||
- ✓ 0026-split-runtime — 5 commits
|
||||
- ✓ 0027-prune-config-schema — 3 commits
|
||||
- ✓ 0031-expand-file-tools — 7 commits
|
||||
|
||||
### Issues fallidos
|
||||
- ✗ 0029-core-tests — falló en fase de tests (excluido)
|
||||
|
||||
### Estado de master
|
||||
- Build: OK
|
||||
- Tests: OK (142 passed)
|
||||
- Commits nuevos: 18
|
||||
|
||||
### Siguiente paso
|
||||
Ejecutar: git push
|
||||
```
|
||||
|
||||
## Notas importantes
|
||||
|
||||
- **Stack agnostico**: la skill detecta el stack (Go, C++, Rust, Node, Python) en Fase 3. Si la auto-deteccion falla o el proyecto es exotico, el usuario puede pasar `BUILD_CMD`/`TEST_CMD` por env var o crear `.parallel-fix-issues.yml` en la raiz. Si el proyecto no tiene build/test, esos pasos se omiten con WARN
|
||||
- **Siempre usar `dangerouslyDisableSandbox: true`** en todas las llamadas Bash de los agentes paralelos
|
||||
- **Nunca hacer push automáticamente** — el usuario decide cuándo pushear
|
||||
- **Si hay merge conflicts**, parar y pedir intervención manual
|
||||
- **Un worktree = un issue = una branch** — nunca mezclar
|
||||
- Los worktrees se crean desde `master` actualizado
|
||||
- La carpeta `worktrees/` está en `.gitignore`
|
||||
- Issues con dependencias externas no resueltas se excluyen automáticamente
|
||||
- **README centralizado**: los agentes NO tocan `dev/issues/README.md` — solo el orquestador lo actualiza después del merge, en un solo commit. Esto evita merge conflicts entre agentes paralelos
|
||||
- **`git mv` para cerrar issues**: usar `git mv` (no `mv` + `git add`) para mover issues a `completed/`
|
||||
|
||||
## Casos de uso
|
||||
|
||||
```
|
||||
# Implementar todos los issues pendientes
|
||||
/parallel-fix-issues all
|
||||
|
||||
# Implementar issues específicos
|
||||
/parallel-fix-issues 0026 0027 0031
|
||||
|
||||
# Solo los issues de refactor
|
||||
/parallel-fix-issues 0026 0027 0028
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# integrate-worktrees.sh — Integra branches de worktrees a master con --no-ff
|
||||
#
|
||||
# Uso: ./integrate-worktrees.sh <slug-1> <slug-2> ...
|
||||
# Ejemplo: ./integrate-worktrees.sh 0026-split-runtime 0027-prune-config-schema
|
||||
#
|
||||
# Para cada slug:
|
||||
# 1. git merge --no-ff issue/<slug> a master
|
||||
# 2. Verificar que master compila después del merge
|
||||
# 3. Si hay conflict o fallo de build, PARAR inmediatamente
|
||||
#
|
||||
# Los slugs deben pasarse en el orden correcto (waves ya resueltas).
|
||||
# NO hace push — eso lo decide el usuario.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ERROR: se necesita al menos un slug"
|
||||
echo "Uso: $0 <slug-1> <slug-2> ..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Asegurar que estamos en master
|
||||
echo "=== Cambiando a master ==="
|
||||
cd "$REPO_ROOT"
|
||||
git checkout master
|
||||
|
||||
MERGED=0
|
||||
FAILED_AT=""
|
||||
|
||||
for slug in "$@"; do
|
||||
branch="issue/${slug}"
|
||||
|
||||
echo ""
|
||||
echo "=== Integrando: ${branch} ==="
|
||||
|
||||
# Verificar que la branch existe
|
||||
if ! git show-ref --verify --quiet "refs/heads/${branch}"; then
|
||||
echo "FAIL: branch ${branch} no existe"
|
||||
FAILED_AT="$slug"
|
||||
break
|
||||
fi
|
||||
|
||||
# Merge --no-ff
|
||||
if ! git merge --no-ff "$branch" -m "merge: ${branch} — implementación paralela"; then
|
||||
echo ""
|
||||
echo "CONFLICT: merge de ${branch} tiene conflictos"
|
||||
echo "Resolver manualmente y luego continuar con los slugs restantes"
|
||||
echo ""
|
||||
echo "Para resolver:"
|
||||
echo " 1. git status (ver archivos en conflicto)"
|
||||
echo " 2. Resolver conflictos en cada archivo"
|
||||
echo " 3. git add <archivos>"
|
||||
echo " 4. git commit"
|
||||
echo ""
|
||||
echo "Slugs pendientes después de ${slug}:"
|
||||
FOUND=0
|
||||
for remaining in "$@"; do
|
||||
if [ "$FOUND" -eq 1 ]; then
|
||||
echo " - ${remaining}"
|
||||
fi
|
||||
if [ "$remaining" = "$slug" ]; then
|
||||
FOUND=1
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "MERGED: ${branch}"
|
||||
|
||||
# Verificar que master sigue compilando (si BUILD_CMD esta definido)
|
||||
if [ -n "${BUILD_CMD:-}" ]; then
|
||||
echo "--- Verificando build post-merge ($BUILD_CMD) ---"
|
||||
if ! (cd "$REPO_ROOT" && bash -c "$BUILD_CMD" 2>&1); then
|
||||
echo ""
|
||||
echo "FAIL: master no compila despues de mergear ${branch}"
|
||||
echo "Revertir con: git reset --hard HEAD~1"
|
||||
echo "Investigar el problema antes de continuar."
|
||||
FAILED_AT="$slug"
|
||||
break
|
||||
fi
|
||||
echo "OK: build post-merge exitoso"
|
||||
else
|
||||
echo "--- Build post-merge SKIPPED (BUILD_CMD no definido) ---"
|
||||
fi
|
||||
|
||||
MERGED=$((MERGED + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen de integración ==="
|
||||
echo "Mergeados: ${MERGED} de $#"
|
||||
|
||||
if [ -n "$FAILED_AT" ]; then
|
||||
echo "Falló en: ${FAILED_AT}"
|
||||
echo ""
|
||||
echo "Worktrees NO limpiados (resolver primero el fallo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Limpieza de worktrees y branches
|
||||
echo ""
|
||||
echo "=== Limpieza ==="
|
||||
for slug in "$@"; do
|
||||
path="${REPO_ROOT}/worktrees/${slug}"
|
||||
branch="issue/${slug}"
|
||||
|
||||
if [ -d "$path" ]; then
|
||||
git worktree remove "$path" 2>/dev/null && echo "REMOVED: worktree ${path}" || echo "WARN: no se pudo eliminar worktree ${path}"
|
||||
fi
|
||||
|
||||
git branch -d "$branch" 2>/dev/null && echo "DELETED: branch ${branch}" || echo "WARN: no se pudo eliminar branch ${branch}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Integración completa ==="
|
||||
echo "Master tiene ${MERGED} merges nuevos."
|
||||
echo ""
|
||||
echo "Para publicar: git push"
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# setup-worktrees.sh — Crea git worktrees para ejecución paralela de issues
|
||||
#
|
||||
# Uso: ./setup-worktrees.sh <slug-1> <slug-2> ...
|
||||
# Ejemplo: ./setup-worktrees.sh 0026-split-runtime 0027-prune-config-schema
|
||||
#
|
||||
# Cada slug genera:
|
||||
# worktrees/<slug>/ (worktree completo)
|
||||
# branch: issue/<slug>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKTREE_DIR="${REPO_ROOT}/worktrees"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ERROR: se necesita al menos un slug de issue"
|
||||
echo "Uso: $0 <slug-1> <slug-2> ..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Asegurar que master está actualizado
|
||||
echo "=== Actualizando master ==="
|
||||
CURRENT_BRANCH="$(git branch --show-current)"
|
||||
git checkout master 2>/dev/null
|
||||
git pull --rebase 2>/dev/null || echo "WARN: no se pudo pull (sin remote o sin conexión)"
|
||||
|
||||
# Volver a la rama original si no era master
|
||||
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
|
||||
git checkout "$CURRENT_BRANCH" 2>/dev/null
|
||||
fi
|
||||
|
||||
mkdir -p "$WORKTREE_DIR"
|
||||
|
||||
CREATED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
|
||||
for slug in "$@"; do
|
||||
branch="issue/${slug}"
|
||||
path="${WORKTREE_DIR}/${slug}"
|
||||
|
||||
if [ -d "$path" ]; then
|
||||
echo "SKIP: worktree ya existe: ${path}"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Verificar que la branch no existe ya
|
||||
if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
|
||||
echo "WARN: branch ${branch} ya existe, creando worktree desde ella"
|
||||
git worktree add "$path" "$branch" 2>/dev/null || {
|
||||
echo "FAIL: no se pudo crear worktree para ${slug}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
}
|
||||
else
|
||||
echo "CREATE: worktree ${path} (branch ${branch})"
|
||||
git worktree add -b "$branch" "$path" master 2>/dev/null || {
|
||||
echo "FAIL: no se pudo crear worktree para ${slug}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
}
|
||||
fi
|
||||
|
||||
CREATED=$((CREATED + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen ==="
|
||||
echo "Creados: ${CREATED}"
|
||||
echo "Existentes: ${SKIPPED}"
|
||||
echo "Fallidos: ${FAILED}"
|
||||
echo ""
|
||||
echo "=== Worktrees activos ==="
|
||||
git worktree list
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/bin/bash
|
||||
# verify-worktree.sh — Verifica build, tests y cierre de issue en un worktree.
|
||||
#
|
||||
# Uso:
|
||||
# ./verify-worktree.sh <worktree-path> [build-cmd] [test-cmd]
|
||||
#
|
||||
# Ejemplos:
|
||||
# ./verify-worktree.sh worktrees/0026-foo
|
||||
# ./verify-worktree.sh worktrees/0026-foo "go build -tags fts5 ./..." "go test -tags fts5 ./..."
|
||||
# BUILD_CMD="cmake --build cpp/build" TEST_CMD="ctest --test-dir cpp/build" ./verify-worktree.sh worktrees/0026-foo
|
||||
#
|
||||
# Resolucion de comandos (en orden de prioridad):
|
||||
# 1. Argumentos posicionales (build-cmd, test-cmd)
|
||||
# 2. Variables de entorno BUILD_CMD / TEST_CMD
|
||||
# 3. Archivo .parallel-fix-issues.yml en la raiz del worktree (claves: build, test)
|
||||
# 4. Auto-deteccion segun ficheros del proyecto:
|
||||
# - go.mod → "go build ./..." + "go test ./..."
|
||||
# - CMakeLists.txt → "cmake -S . -B build && cmake --build build" + "ctest --test-dir build"
|
||||
# - Cargo.toml → "cargo build" + "cargo test"
|
||||
# - package.json → "npm run build" + "npm test"
|
||||
# - pyproject.toml → "" + "pytest"
|
||||
# 5. Si nada se detecta, salta build/test con WARN.
|
||||
#
|
||||
# Auto-deteccion adicional: si hay go.mod, intenta extraer build tag de //go:build.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = todo OK
|
||||
# 1 = error de argumento
|
||||
# 2 = build fallo
|
||||
# 3 = tests fallaron
|
||||
# 4 = issue no cerrado (solo WARN, no falla)
|
||||
# 5 = sin commits propios
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "ERROR: se necesita el path del worktree"
|
||||
echo "Uso: $0 <worktree-path> [build-cmd] [test-cmd]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKTREE="$1"
|
||||
ARG_BUILD_CMD="${2:-}"
|
||||
ARG_TEST_CMD="${3:-}"
|
||||
|
||||
# Resolver path absoluto
|
||||
if [[ "$WORKTREE" != /* ]]; then
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKTREE="${REPO_ROOT}/${WORKTREE}"
|
||||
fi
|
||||
|
||||
if [ ! -d "$WORKTREE" ]; then
|
||||
echo "ERROR: worktree no encontrado: ${WORKTREE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLUG="$(basename "$WORKTREE")"
|
||||
echo "=== Verificando: ${SLUG} ==="
|
||||
|
||||
# --- Resolver build/test commands ---
|
||||
BUILD_CMD="${ARG_BUILD_CMD:-${BUILD_CMD:-}}"
|
||||
TEST_CMD="${ARG_TEST_CMD:-${TEST_CMD:-}}"
|
||||
|
||||
# Manifest opcional
|
||||
MANIFEST="${WORKTREE}/.parallel-fix-issues.yml"
|
||||
if [ -z "$BUILD_CMD" ] && [ -f "$MANIFEST" ]; then
|
||||
M_BUILD=$(grep -E "^build:" "$MANIFEST" 2>/dev/null | sed -E 's/^build:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
|
||||
if [ -n "$M_BUILD" ]; then BUILD_CMD="$M_BUILD"; echo "INFO: build desde manifest"; fi
|
||||
fi
|
||||
if [ -z "$TEST_CMD" ] && [ -f "$MANIFEST" ]; then
|
||||
M_TEST=$(grep -E "^test:" "$MANIFEST" 2>/dev/null | sed -E 's/^test:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
|
||||
if [ -n "$M_TEST" ]; then TEST_CMD="$M_TEST"; echo "INFO: test desde manifest"; fi
|
||||
fi
|
||||
|
||||
# Auto-deteccion
|
||||
if [ -z "$BUILD_CMD" ] || [ -z "$TEST_CMD" ]; then
|
||||
AUTO_BUILD=""
|
||||
AUTO_TEST=""
|
||||
if [ -f "${WORKTREE}/go.mod" ]; then
|
||||
# Detectar build tag
|
||||
AUTO_TAG=$(grep -rh "^//go:build " --include="*.go" "$WORKTREE" 2>/dev/null \
|
||||
| sed -E 's|^//go:build ([a-zA-Z0-9_]+).*|\1|' \
|
||||
| sort -u | head -1 || true)
|
||||
TAG_FLAG=""
|
||||
[ -n "$AUTO_TAG" ] && TAG_FLAG="-tags $AUTO_TAG"
|
||||
AUTO_BUILD="go build $TAG_FLAG ./..."
|
||||
AUTO_TEST="go test $TAG_FLAG ./..."
|
||||
echo "INFO: stack detectado: Go${TAG_FLAG:+ ($TAG_FLAG)}"
|
||||
elif [ -f "${WORKTREE}/CMakeLists.txt" ] || ls "${WORKTREE}"/cpp/CMakeLists.txt >/dev/null 2>&1; then
|
||||
CMAKE_DIR="."
|
||||
[ -f "${WORKTREE}/cpp/CMakeLists.txt" ] && [ ! -f "${WORKTREE}/CMakeLists.txt" ] && CMAKE_DIR="cpp"
|
||||
AUTO_BUILD="cmake -S ${CMAKE_DIR} -B ${CMAKE_DIR}/build -DCMAKE_BUILD_TYPE=Release && cmake --build ${CMAKE_DIR}/build -j"
|
||||
AUTO_TEST="ctest --test-dir ${CMAKE_DIR}/build --output-on-failure || true"
|
||||
echo "INFO: stack detectado: C++/CMake (dir=${CMAKE_DIR})"
|
||||
elif [ -f "${WORKTREE}/Cargo.toml" ]; then
|
||||
AUTO_BUILD="cargo build"
|
||||
AUTO_TEST="cargo test"
|
||||
echo "INFO: stack detectado: Rust"
|
||||
elif [ -f "${WORKTREE}/package.json" ]; then
|
||||
AUTO_BUILD="npm run build --if-present"
|
||||
AUTO_TEST="npm test --if-present"
|
||||
echo "INFO: stack detectado: Node"
|
||||
elif [ -f "${WORKTREE}/pyproject.toml" ] || [ -f "${WORKTREE}/setup.py" ]; then
|
||||
AUTO_BUILD="" # python normalmente no tiene build step
|
||||
AUTO_TEST="pytest"
|
||||
echo "INFO: stack detectado: Python"
|
||||
else
|
||||
echo "WARN: no se detecto stack; usar BUILD_CMD/TEST_CMD env o manifest .parallel-fix-issues.yml"
|
||||
fi
|
||||
[ -z "$BUILD_CMD" ] && BUILD_CMD="$AUTO_BUILD"
|
||||
[ -z "$TEST_CMD" ] && TEST_CMD="$AUTO_TEST"
|
||||
fi
|
||||
|
||||
# 1. Verificar commits propios
|
||||
echo ""
|
||||
echo "--- Commits propios ---"
|
||||
COMMIT_COUNT=$(cd "$WORKTREE" && git log master..HEAD --oneline 2>/dev/null | wc -l)
|
||||
if [ "$COMMIT_COUNT" -eq 0 ]; then
|
||||
echo "FAIL: sin commits propios en la branch"
|
||||
exit 5
|
||||
fi
|
||||
echo "OK: ${COMMIT_COUNT} commits desde master"
|
||||
cd "$WORKTREE" && git log master..HEAD --oneline
|
||||
|
||||
# 2. Build
|
||||
echo ""
|
||||
if [ -n "$BUILD_CMD" ]; then
|
||||
echo "--- Build ($BUILD_CMD) ---"
|
||||
if (cd "$WORKTREE" && bash -c "$BUILD_CMD" 2>&1); then
|
||||
echo "OK: build exitoso"
|
||||
else
|
||||
echo "FAIL: build fallo"
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "--- Build SKIPPED (sin comando) ---"
|
||||
fi
|
||||
|
||||
# 3. Tests
|
||||
echo ""
|
||||
if [ -n "$TEST_CMD" ]; then
|
||||
echo "--- Tests ($TEST_CMD) ---"
|
||||
if (cd "$WORKTREE" && bash -c "$TEST_CMD" 2>&1); then
|
||||
echo "OK: tests pasaron"
|
||||
else
|
||||
echo "FAIL: tests fallaron"
|
||||
exit 3
|
||||
fi
|
||||
else
|
||||
echo "--- Tests SKIPPED (sin comando) ---"
|
||||
fi
|
||||
|
||||
# 4. Issue cerrado
|
||||
echo ""
|
||||
echo "--- Cierre de issue ---"
|
||||
COMPLETED_FILES=$(cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/ 2>/dev/null | wc -l)
|
||||
if [ "$COMPLETED_FILES" -gt 0 ]; then
|
||||
echo "OK: issue movido a completed/"
|
||||
cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/
|
||||
else
|
||||
echo "WARN: no se detecto issue movido a completed/ (verificar manualmente)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== RESULTADO: ${SLUG} — OK ==="
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
name: parallel-issues
|
||||
description: Analiza issues y genera plan de ejecución paralela en PARALLEL_EXECUTION_ORDER.md
|
||||
argument-hint: [--dry-run]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write
|
||||
---
|
||||
|
||||
# parallel-issues
|
||||
|
||||
Analiza issues pendientes y genera plan de ejecución paralela agrupando issues independientes.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/parallel-issues # Genera archivo
|
||||
/parallel-issues --dry-run # Solo muestra análisis
|
||||
```
|
||||
|
||||
## Cuándo usar
|
||||
|
||||
- Identificar issues paralelizables sin conflictos
|
||||
- Planificar desarrollo con múltiples worktrees
|
||||
- Antes de sesiones intensivas de desarrollo
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Detectar contexto
|
||||
|
||||
```bash
|
||||
# Proyecto padre o hijo?
|
||||
if [[ "$PWD" == *"/workspaces/"* ]]; then
|
||||
PROJECT_TYPE="child"
|
||||
else
|
||||
PROJECT_TYPE="parent"
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. Listar issues pendientes
|
||||
|
||||
```bash
|
||||
ls -1 dev/issues/*.md | grep -E '[0-9]{4}-.*\.md$' | sort
|
||||
```
|
||||
|
||||
Para cada issue extraer:
|
||||
- Número, título, estado
|
||||
- Archivos mencionados
|
||||
- Dependencias explícitas (#NNNN)
|
||||
|
||||
### 3. Analizar conflictos
|
||||
|
||||
**Criterios de conflicto (NO paralelizables):**
|
||||
- Archivos compartidos
|
||||
- Dependencias explícitas
|
||||
- Dependencias transitivas
|
||||
|
||||
### 4. Agrupar por independencia
|
||||
|
||||
Algoritmo de grafos:
|
||||
- Grupo 1: Issues sin dependencias
|
||||
- Grupo 2: Issues que dependen solo de Grupo 1
|
||||
- etc.
|
||||
|
||||
### 5. Estimar tiempos
|
||||
|
||||
Factores:
|
||||
- Cantidad de archivos
|
||||
- Capa afectada (core/shell/app)
|
||||
- Palabras clave (refactor, fix, nuevo)
|
||||
|
||||
### 6. Generar PARALLEL_EXECUTION_ORDER.md
|
||||
|
||||
```markdown
|
||||
# Plan de Ejecución Paralela
|
||||
|
||||
## Grupo 1: Issues Independientes
|
||||
- Issue #0003 - ...
|
||||
- Issue #0006 - ...
|
||||
|
||||
## Grupo 2: Dependientes de Grupo 1
|
||||
- Issue #0004 - depende de #0003
|
||||
|
||||
## Resumen
|
||||
| Métrica | Valor |
|
||||
|---------|-------|
|
||||
| Ahorro tiempo | 60% |
|
||||
```
|
||||
|
||||
### 7. Mostrar resultado
|
||||
|
||||
```
|
||||
Plan generado: PARALLEL_EXECUTION_ORDER.md
|
||||
|
||||
Issues analizadas: N
|
||||
Grupos paralelos: M
|
||||
Ahorro estimado: X%
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Nombres de grupo: "Grupo N"
|
||||
- Worktrees: `worktrees/issue-NNNN`
|
||||
- Estimación en horas (redondeado a .5)
|
||||
@@ -0,0 +1,190 @@
|
||||
---
|
||||
name: pass-usage
|
||||
description: Gestiona contraseñas con pass (password-store) y GPG. Inserta, lista, busca, genera y sincroniza secretos con Gitea.
|
||||
argument-hint: [accion] [nombre-secreto]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write
|
||||
---
|
||||
|
||||
# pass-usage
|
||||
|
||||
Gestiona contraseñas usando `pass` (password-store) con cifrado GPG, sincronizando con Gitea via `pass git push`.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/pass-usage # Listar todos los secretos
|
||||
/pass-usage add servicio/usuario # Agregar nuevo secreto
|
||||
/pass-usage show servicio/usuario # Ver un secreto
|
||||
/pass-usage gen servicio/usuario 24 # Generar contraseña aleatoria (24 chars)
|
||||
/pass-usage find texto # Buscar secretos por nombre
|
||||
/pass-usage sync # Sincronizar con Gitea (push + pull)
|
||||
/pass-usage edit servicio/usuario # Editar secreto existente
|
||||
/pass-usage rm servicio/usuario # Eliminar secreto
|
||||
/pass-usage help # Mostrar esta ayuda
|
||||
```
|
||||
|
||||
## Precondiciones
|
||||
|
||||
Verificar antes de cualquier operación:
|
||||
|
||||
```bash
|
||||
# 1. Verificar pass instalado
|
||||
if ! command -v pass &>/dev/null; then
|
||||
echo "pass no está instalado."
|
||||
echo "Ejecuta: sudo apt install pass # Debian/Ubuntu"
|
||||
echo " o: sudo pacman -S pass # Arch"
|
||||
echo " o: sudo dnf install pass # Fedora"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Verificar GPG disponible
|
||||
if ! command -v gpg &>/dev/null; then
|
||||
echo "gpg no está instalado."
|
||||
echo "Ejecuta: sudo apt install gnupg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Verificar store inicializado
|
||||
if [ ! -d ~/.password-store ]; then
|
||||
echo "Password store no inicializado."
|
||||
echo "Primero necesitas una clave GPG. Listar existentes:"
|
||||
echo " gpg --list-keys"
|
||||
echo ""
|
||||
echo "Si no tienes clave GPG, crear una:"
|
||||
echo " gpg --full-generate-key"
|
||||
echo ""
|
||||
echo "Luego inicializar pass con el ID de tu clave:"
|
||||
echo " pass init <GPG-ID>"
|
||||
echo ""
|
||||
echo "Para habilitar sincronización con Gitea:"
|
||||
echo " pass git init"
|
||||
echo " pass git remote add origin <URL-REPO-GITEA>"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
Parsear `$ARGUMENTS` para determinar la acción. Si no hay argumentos, listar secretos.
|
||||
|
||||
### Determinar acción
|
||||
|
||||
| $0 (acción) | Operación |
|
||||
|--------------|-----------|
|
||||
| (vacío) | Listar todos |
|
||||
| `add` | Insertar nuevo secreto |
|
||||
| `show` | Mostrar secreto |
|
||||
| `gen` | Generar contraseña aleatoria |
|
||||
| `find` | Buscar por nombre |
|
||||
| `sync` | Push + Pull con Gitea |
|
||||
| `edit` | Editar existente |
|
||||
| `rm` | Eliminar secreto |
|
||||
| `help` | Mostrar ayuda |
|
||||
|
||||
### Acción: Listar (sin argumentos)
|
||||
|
||||
```bash
|
||||
pass
|
||||
```
|
||||
|
||||
Mostrar el árbol de secretos al usuario.
|
||||
|
||||
### Acción: add
|
||||
|
||||
Nombre del secreto: `$1` (requerido).
|
||||
|
||||
Preguntar al usuario qué valor quiere guardar. Luego:
|
||||
|
||||
```bash
|
||||
# Insertar contraseña (interactivo - el usuario debe ejecutarlo)
|
||||
echo "Ejecuta en tu terminal:"
|
||||
echo " pass insert $1"
|
||||
```
|
||||
|
||||
**IMPORTANTE**: `pass insert` es interactivo (pide input por stdin). Indicar al usuario que lo ejecute con `!` o darle la opción de insertar de forma no-interactiva:
|
||||
|
||||
```bash
|
||||
# Opción no-interactiva (si el usuario proporciona el valor)
|
||||
echo "VALOR_SECRETO" | pass insert -e $1
|
||||
```
|
||||
|
||||
Después de insertar, ofrecer sincronizar:
|
||||
|
||||
```bash
|
||||
pass git push
|
||||
```
|
||||
|
||||
### Acción: show
|
||||
|
||||
```bash
|
||||
pass show $1
|
||||
```
|
||||
|
||||
Mostrar el contenido descifrado al usuario.
|
||||
|
||||
### Acción: gen
|
||||
|
||||
Nombre: `$1`, longitud: `$2` (default 20).
|
||||
|
||||
```bash
|
||||
pass generate $1 ${2:-20}
|
||||
```
|
||||
|
||||
Esto genera y guarda una contraseña aleatoria. Ofrecer sincronizar después.
|
||||
|
||||
### Acción: edit
|
||||
|
||||
```bash
|
||||
# Interactivo - el usuario debe ejecutarlo
|
||||
echo "Ejecuta: ! pass edit $1"
|
||||
```
|
||||
|
||||
### Acción: find
|
||||
|
||||
```bash
|
||||
pass find $1
|
||||
```
|
||||
|
||||
### Acción: rm
|
||||
|
||||
**Confirmar con el usuario antes de eliminar.**
|
||||
|
||||
```bash
|
||||
pass rm $1
|
||||
pass git push
|
||||
```
|
||||
|
||||
### Acción: sync
|
||||
|
||||
```bash
|
||||
pass git pull --rebase && pass git push
|
||||
```
|
||||
|
||||
Si falla el pull, mostrar el error y sugerir resolución.
|
||||
|
||||
### Acción: help
|
||||
|
||||
Mostrar la tabla de sintaxis de este skill.
|
||||
|
||||
## Post-operación
|
||||
|
||||
Después de cualquier operación que modifique el store (add, gen, edit, rm):
|
||||
|
||||
1. Mostrar resultado de la operación
|
||||
2. Preguntar si quiere sincronizar con Gitea (`pass git push`)
|
||||
3. Si el repo de secretos en Gitea (`dataforge/pass-secrets`) necesita actualización del README, usar el agente @gitea para actualizarlo
|
||||
|
||||
## Integración con Gitea
|
||||
|
||||
El repo de secretos vive en `dataforge/pass-secrets`. Para operaciones que requieran actualizar documentación o estructura en remoto, delegar al agente @gitea.
|
||||
|
||||
## Reglas
|
||||
|
||||
- NUNCA mostrar contraseñas en texto plano en el output sin que el usuario lo pida explícitamente (acción `show`)
|
||||
- Para `pass insert` y `pass edit`, indicar al usuario que ejecute el comando con `!` ya que son interactivos
|
||||
- Siempre verificar precondiciones antes de operar
|
||||
- Si `pass` o `gpg` no están instalados, dar los comandos de instalación para la distro detectada (no ejecutar sudo directamente)
|
||||
- Ofrecer `pass git push` después de cada modificación
|
||||
- Para obtener el GPG-ID del usuario actual: leerlo de `~/.password-store/.gpg-id` (ese archivo lo crea `pass init <gpg-id>` y contiene el keygrip/ID en uso). Si no existe, listar claves con `gpg --list-secret-keys --keyid-format=long` y pedir al usuario cuál usar
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
name: quick-issue
|
||||
description: Crea un issue automáticamente desde TUI con detección automática de número
|
||||
argument-hint: --text "descripción"
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# quick-issue
|
||||
|
||||
Crea un issue rápido desde TUI. **No invocar manualmente** - es para uso automático.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/quick-issue --text "descripción del issue"
|
||||
```
|
||||
|
||||
## Precondiciones
|
||||
|
||||
- [ ] Directorio `dev/issues/` existe
|
||||
- [ ] Parámetro `--text` proporcionado
|
||||
- [ ] Working tree limpio
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Determinar número
|
||||
|
||||
```bash
|
||||
ls -1 dev/issues/*.md | grep -E '^dev/issues/[0-9]{4}[a-z]?-' | sort -V
|
||||
```
|
||||
|
||||
Siguiente = último número base + 1 (ignorar letras).
|
||||
|
||||
### 2. Generar título y slug
|
||||
|
||||
- Título: usar `--text` directamente
|
||||
- Slug: convertir a kebab-case
|
||||
|
||||
### 3. Crear archivo de issue
|
||||
|
||||
Template minimalista con:
|
||||
- Metadata básica
|
||||
- Objetivo = texto del parámetro
|
||||
- Tareas a completar con /fix-issue
|
||||
|
||||
### 4. Actualizar índice
|
||||
|
||||
Agregar línea en `dev/issues/README.md`.
|
||||
|
||||
### 5. Crear commits y mergear (sin confirmación)
|
||||
|
||||
```bash
|
||||
git checkout -b quick/quick-issue-NNNN
|
||||
git add dev/issues/NNNN-slug.md dev/issues/README.md
|
||||
git commit -m "docs: crear issue NNNN-slug"
|
||||
git checkout master
|
||||
git merge --no-ff quick/quick-issue-NNNN
|
||||
git push
|
||||
git branch -d quick/quick-issue-NNNN
|
||||
```
|
||||
|
||||
### 6. Reportar resultado
|
||||
|
||||
```
|
||||
Issue NNNN-slug creado e integrado
|
||||
|
||||
Para implementar:
|
||||
/fix-issue NNNN
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Auto-detección de número
|
||||
- Sin confirmación (flujo automático)
|
||||
- Template minimalista
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: sino
|
||||
description: Modo respuesta corta — solo si/no/ok/nope/yes/no o frases muy breves para iterar dudas rapidas. One-shot.
|
||||
argument-hint: [pregunta]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# sino
|
||||
|
||||
Modo **respuesta minima** para iterar dudas rapidas. Una sola pregunta = una sola respuesta corta.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/sino <pregunta>
|
||||
```
|
||||
|
||||
## Reglas de respuesta
|
||||
|
||||
- Output al usuario: **solo** una de estas formas:
|
||||
- "si" / "no"
|
||||
- "yes" / "no" / "nope" / "ok" / "yep"
|
||||
- frase muy breve (≤ ~8 palabras) cuando un binario puro pierde info critica
|
||||
- Puedes pensar / razonar internamente lo que necesites antes de responder.
|
||||
- Puedes usar tools de lectura (Read/Grep/Glob/Bash read-only) si la pregunta requiere comprobar algo del repo antes de contestar.
|
||||
- **NO** expliques, **NO** justifiques, **NO** añadas contexto, **NO** ofrezcas alternativas.
|
||||
- **NO** preguntes de vuelta salvo que la pregunta sea literalmente incontestable; en ese caso responde "?" o "ambiguo".
|
||||
|
||||
## One-shot
|
||||
|
||||
Aplica SOLO al turno en que se invoca `/sino`. Siguiente turno = comportamiento normal sin necesidad de "stop sino".
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```
|
||||
/sino existe la funcion filter_slice_go_core?
|
||||
→ si
|
||||
|
||||
/sino deberia mergear esta rama ya?
|
||||
→ no
|
||||
|
||||
/sino kanban usa migraciones?
|
||||
→ si
|
||||
|
||||
/sino esto es seguro borrar /var?
|
||||
→ no, jamas
|
||||
|
||||
/sino que hora es?
|
||||
→ ?
|
||||
```
|
||||
|
||||
## Prioridad
|
||||
|
||||
Si el usuario despues pide explicacion ("por que?", "explica"), salir de `/sino` y responder normal en ese siguiente turno.
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
name: sort-issues
|
||||
description: Analiza dependencias y genera orden de ejecución óptimo de issues
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write
|
||||
---
|
||||
|
||||
# sort-issues
|
||||
|
||||
Analiza issues, construye grafo de dependencias y muestra/genera orden de ejecución recomendado.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/sort-issues
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Listar issues pendientes
|
||||
|
||||
```bash
|
||||
ls dev/issues/*.md | grep -E '^dev/issues/[0-9]{4}[a-z]?-.*\.md$' | sort
|
||||
```
|
||||
|
||||
### 2. Extraer dependencias de cada issue
|
||||
|
||||
Buscar:
|
||||
- Tabla "## Dependencias"
|
||||
- Línea "Bloqueada por"
|
||||
- Referencias #NNNN
|
||||
|
||||
### 3. Construir grafo y detectar ciclos
|
||||
|
||||
Si hay ciclos:
|
||||
```
|
||||
Dependencias circulares detectadas:
|
||||
0010 → 0011 → 0012 → 0010
|
||||
|
||||
Revisar:
|
||||
- dev/issues/0010-*.md
|
||||
- dev/issues/0011-*.md
|
||||
```
|
||||
|
||||
### 4. Calcular orden topológico
|
||||
|
||||
Algoritmo Kahn o DFS post-order.
|
||||
|
||||
Desempate:
|
||||
1. Número menor primero
|
||||
2. Issues sin deps primero
|
||||
|
||||
### 5. Generar EXECUTION_ORDER.md
|
||||
|
||||
```markdown
|
||||
# Execution Order
|
||||
|
||||
## Recommended Order
|
||||
1. #0001 - titulo — razón
|
||||
2. #0002 - titulo — razón
|
||||
|
||||
## Critical Path
|
||||
- #0001 → #0002, #0003
|
||||
|
||||
## Parallelizable Groups
|
||||
### Group 1 (after #0001)
|
||||
- #0002
|
||||
- #0003
|
||||
```
|
||||
|
||||
### 6. Mostrar resultado
|
||||
|
||||
```
|
||||
Orden generado: dev/EXECUTION_ORDER.md
|
||||
|
||||
Issues: N
|
||||
Camino crítico: #X → #Y → #Z
|
||||
Grupos paralelos: M
|
||||
|
||||
Próxima issue: #0001 - titulo
|
||||
```
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Solo leer issues (no modificar)
|
||||
- Detectar ambos formatos de dependencias
|
||||
- Reportar ciclos claramente
|
||||
+171
-18
@@ -23,6 +23,11 @@ MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "Unknown"')
|
||||
CONTEXT_PCT=$(echo "$INPUT" | jq -r '.context_window.used_percentage // 0' | xargs printf "%.0f")
|
||||
CONTEXT_TOTAL=$(echo "$INPUT" | jq -r '.context_window.context_window_size // 200000')
|
||||
CURRENT_DIR=$(echo "$INPUT" | jq -r '.workspace.current_dir // "~"' | sed "s|$HOME|~|")
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
|
||||
|
||||
# Purga: borra goal files de sesiones muertas (no tocados en >7 dias). El worker
|
||||
# refresca el mtime en cada respuesta, asi que las sesiones vivas nunca caen.
|
||||
find "$HOME/.claude/goals" -maxdepth 1 -name '*.json' -mtime +7 -delete 2>/dev/null
|
||||
|
||||
# Tokens de entrada y salida (current_usage puede ser null antes del primer API call)
|
||||
INPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.input_tokens // 0')
|
||||
@@ -40,6 +45,25 @@ if [ "$CONTEXT_PCT" -eq 0 ] && [ "$CONTEXT_USED" -gt 0 ]; then
|
||||
CONTEXT_PCT=$(echo "scale=0; $CONTEXT_USED * 100 / $CONTEXT_TOTAL" | bc)
|
||||
fi
|
||||
|
||||
# Persistir el contexto por sesión en un sidecar para que fleetview (y otras
|
||||
# herramientas) puedan mostrarlo sin tener este stdin. El statusline se re-ejecuta
|
||||
# cada pocos segundos, así que el dato se mantiene fresco mientras la sesión vive.
|
||||
if [ -n "$SESSION_ID" ]; then
|
||||
RTDIR="$HOME/.claude/runtime"
|
||||
mkdir -p "$RTDIR" 2>/dev/null
|
||||
RTF="$RTDIR/${SESSION_ID}.json"
|
||||
RTMP="${RTF}.tmp.$$"
|
||||
if jq -n \
|
||||
--argjson pct "${CONTEXT_PCT:-0}" \
|
||||
--argjson used "${CONTEXT_USED:-0}" \
|
||||
--argjson total "${CONTEXT_TOTAL:-200000}" \
|
||||
'{ctx_pct:$pct, ctx_used:$used, ctx_total:$total}' > "$RTMP" 2>/dev/null; then
|
||||
mv "$RTMP" "$RTF" 2>/dev/null
|
||||
else
|
||||
rm -f "$RTMP" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Costos
|
||||
TOTAL_COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0' | xargs printf "%.3f")
|
||||
SESSION_DURATION=$(echo "$INPUT" | jq -r '.cost.total_duration_ms // 0')
|
||||
@@ -49,11 +73,27 @@ LINES_REMOVED=$(echo "$INPUT" | jq -r '.cost.total_lines_removed // 0')
|
||||
# Rate Limits
|
||||
RATE_5H=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.used_percentage // 0' | xargs printf "%.0f")
|
||||
RATE_7D=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.used_percentage // 0' | xargs printf "%.0f")
|
||||
RESET_5H_EPOCH=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.resets_at // 0')
|
||||
RESET_7D_EPOCH=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.resets_at // 0')
|
||||
|
||||
# Git info (si estamos en un repo)
|
||||
# Formatear resets (vacio si epoch=0)
|
||||
RESET_5H=""
|
||||
RESET_7D=""
|
||||
[ "$RESET_5H_EPOCH" -gt 0 ] && RESET_5H=$(date -d "@$RESET_5H_EPOCH" +"%H:%M" 2>/dev/null)
|
||||
[ "$RESET_7D_EPOCH" -gt 0 ] && RESET_7D=$(date -d "@$RESET_7D_EPOCH" +"%a %H:%M" 2>/dev/null)
|
||||
|
||||
# Git info (si estamos en un repo). Con cache de TTL corto: como el statusline
|
||||
# se re-ejecuta cada pocos segundos (refreshInterval), recomputar git en cada
|
||||
# tick es caro en repos grandes y el estado git no cambia estando idle. Se cachea
|
||||
# por directorio y se recomputa solo si el cache tiene mas de 6s.
|
||||
GIT_BRANCH=""
|
||||
GIT_STATUS=""
|
||||
if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
GIT_CACHE="/tmp/fn_sl_git_$(printf '%s' "$CURRENT_DIR" | cksum | cut -d' ' -f1).cache"
|
||||
GIT_CACHE_AGE=999
|
||||
[ -f "$GIT_CACHE" ] && GIT_CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$GIT_CACHE" 2>/dev/null || echo 0) ))
|
||||
if [ "$GIT_CACHE_AGE" -lt 6 ]; then
|
||||
. "$GIT_CACHE"
|
||||
elif git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
|
||||
|
||||
# Obtener archivos staged, modified, untracked
|
||||
@@ -81,8 +121,41 @@ if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
|
||||
# Trim trailing space
|
||||
GIT_STATUS=$(echo "$GIT_STATUS" | sed 's/ $//')
|
||||
|
||||
# Guardar en cache (quoting seguro para re-source).
|
||||
printf 'GIT_BRANCH=%q\nGIT_STATUS=%q\n' "$GIT_BRANCH" "$GIT_STATUS" > "$GIT_CACHE" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Color estable por sesión (hash del session_id → paleta ANSI 256 legible).
|
||||
# Cada terminal mantiene su color toda su vida; distinto entre terminales.
|
||||
goal_color() {
|
||||
local sid="$1"
|
||||
local palette=(39 45 51 75 81 114 120 156 183 210 215 222 213 159 228)
|
||||
local h
|
||||
h=$(printf '%s' "$sid" | cksum | cut -d' ' -f1)
|
||||
local idx=$(( h % ${#palette[@]} ))
|
||||
printf '\033[1;38;5;%dm' "${palette[$idx]}"
|
||||
}
|
||||
|
||||
# Fase de trabajo → icono | color ANSI | etiqueta visible.
|
||||
# El slug (clave) lo escribe el agente del Stop hook; aqui se mapea a su estilo.
|
||||
phase_style() {
|
||||
case "$1" in
|
||||
investigando) printf '🔎|36|investigando' ;;
|
||||
planificando) printf '📋|34|planificando' ;;
|
||||
haciendo) printf '🔨|33|haciendo' ;;
|
||||
testeando) printf '🧪|35|testeando' ;;
|
||||
puliendo) printf '✨|95|puliendo detalles' ;;
|
||||
pendiente_revision) printf '👀|93|pendiente de revisión' ;;
|
||||
preguntando) printf '❓|96|esperando respuesta' ;;
|
||||
bloqueado) printf '⛔|31|bloqueado' ;;
|
||||
en_pausa) printf '⏸️|90|en pausa' ;;
|
||||
hecho) printf '✅|32|hecho' ;;
|
||||
iterando) printf '🔁|94|iterando' ;;
|
||||
*) printf "•|90|$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Función para crear barra de progreso
|
||||
progress_bar() {
|
||||
local pct=$1
|
||||
@@ -190,23 +263,49 @@ LINE2="${LINE2} ${GRAY}│${RESET} ${DIM}Total:${RESET} ${CYAN}↓${TOTAL_IN_FMT
|
||||
# 4. Rate Limits (siempre mostrar)
|
||||
LINE2="${LINE2} ${GRAY}│${RESET} ${BOLD}Limits:${RESET}"
|
||||
|
||||
# 5h limit
|
||||
if [ "$RATE_5H" -ge 80 ]; then
|
||||
LINE2="${LINE2} ${RED}5h:${RATE_5H}%${RESET}"
|
||||
elif [ "$RATE_5H" -ge 50 ]; then
|
||||
LINE2="${LINE2} ${YELLOW}5h:${RATE_5H}%${RESET}"
|
||||
else
|
||||
LINE2="${LINE2} ${GREEN}5h:${RATE_5H}%${RESET}"
|
||||
fi
|
||||
# Color por burndown vs tasa esperada
|
||||
# Tasa: % consumible permitido por unidad de tiempo (5h: 20%/h, 7d: 14%/dia)
|
||||
# expected = tasa * unidades_restantes_hasta_reset
|
||||
# available = 100 - used%
|
||||
# verde: available >= expected (consumo bajo control)
|
||||
# amarillo: available >= expected/2 (consumo agresivo)
|
||||
# rojo: available < expected/2 (riesgo de agotar antes del reset)
|
||||
NOW_EPOCH=$(date +%s)
|
||||
burndown_color() {
|
||||
local used_pct=$1
|
||||
local secs_left=$2
|
||||
local rate=$3
|
||||
local secs_per_unit=$4
|
||||
local available=$((100 - used_pct))
|
||||
if [ "$secs_left" -le 0 ]; then
|
||||
printf "%s" "$GREEN"; return
|
||||
fi
|
||||
local expected
|
||||
expected=$(echo "scale=2; $rate * $secs_left / $secs_per_unit" | bc)
|
||||
if (( $(echo "$available >= $expected" | bc -l) )); then
|
||||
printf "%s" "$GREEN"
|
||||
elif (( $(echo "$available >= $expected / 2" | bc -l) )); then
|
||||
printf "%s" "$YELLOW"
|
||||
else
|
||||
printf "%s" "$RED"
|
||||
fi
|
||||
}
|
||||
|
||||
# 7d limit
|
||||
if [ "$RATE_7D" -ge 80 ]; then
|
||||
LINE2="${LINE2} ${RED}7d:${RATE_7D}%${RESET}"
|
||||
elif [ "$RATE_7D" -ge 50 ]; then
|
||||
LINE2="${LINE2} ${YELLOW}7d:${RATE_7D}%${RESET}"
|
||||
else
|
||||
LINE2="${LINE2} ${GREEN}7d:${RATE_7D}%${RESET}"
|
||||
fi
|
||||
# 5h limit (tasa 20%/h)
|
||||
SECS_5H=0
|
||||
[ "$RESET_5H_EPOCH" -gt "$NOW_EPOCH" ] && SECS_5H=$((RESET_5H_EPOCH - NOW_EPOCH))
|
||||
C5=$(burndown_color $RATE_5H $SECS_5H 20 3600)
|
||||
RESET_5H_STR=""
|
||||
[ -n "$RESET_5H" ] && RESET_5H_STR=" ${C5}→${RESET_5H}${RESET}"
|
||||
LINE2="${LINE2} ${C5}5h:${RATE_5H}%${RESET}${RESET_5H_STR} ${GRAY}│${RESET}"
|
||||
|
||||
# 7d limit (tasa 14%/dia)
|
||||
SECS_7D=0
|
||||
[ "$RESET_7D_EPOCH" -gt "$NOW_EPOCH" ] && SECS_7D=$((RESET_7D_EPOCH - NOW_EPOCH))
|
||||
C7=$(burndown_color $RATE_7D $SECS_7D 14 86400)
|
||||
RESET_7D_STR=""
|
||||
[ -n "$RESET_7D" ] && RESET_7D_STR=" ${C7}→${RESET_7D}${RESET}"
|
||||
LINE2="${LINE2} ${C7}7d:${RATE_7D}%${RESET}${RESET_7D_STR}"
|
||||
|
||||
# 5. Duración sesión
|
||||
if [ "$SESSION_DURATION" -gt 0 ]; then
|
||||
@@ -217,6 +316,60 @@ fi
|
||||
# 6. Directorio actual
|
||||
LINE2="${LINE2} ${GRAY}│${RESET} ${BLUE}${CURRENT_DIR}${RESET}"
|
||||
|
||||
# ===== LÍNEA 0: Objetivo (izq) + Fase (der) =====
|
||||
# Solo si la sesión tiene archivo de objetivo con goal no vacío.
|
||||
GOAL_FILE="$HOME/.claude/goals/${SESSION_ID}.json"
|
||||
if [ -n "$SESSION_ID" ] && [ -f "$GOAL_FILE" ]; then
|
||||
GOAL=$(jq -r '.goal // ""' "$GOAL_FILE" 2>/dev/null)
|
||||
PHASE=$(jq -r '.phase // ""' "$GOAL_FILE" 2>/dev/null)
|
||||
DOD=$(jq -r '.dod // ""' "$GOAL_FILE" 2>/dev/null)
|
||||
EMOJIS=$(jq -r '.emojis // ""' "$GOAL_FILE" 2>/dev/null)
|
||||
PROVISIONAL=$(jq -r '.provisional // false' "$GOAL_FILE" 2>/dev/null)
|
||||
if [ -n "$GOAL" ]; then
|
||||
GC=$(goal_color "$SESSION_ID")
|
||||
# Prefijo del objetivo:
|
||||
# - provisional (= tu propio texto, mientras haiku genera el real) -> ⏳ atenuado.
|
||||
# - los 3 emojis generados (representan la tarea, igual que FleetView).
|
||||
# - fallback al marcador generico de objetivo.
|
||||
if [ "$PROVISIONAL" = "true" ]; then
|
||||
LEFT="${GC}⏳ ${DIM}${GOAL}${RESET}"
|
||||
elif [ -n "$EMOJIS" ]; then
|
||||
LEFT="${GC}${EMOJIS} ${GOAL}${RESET}"
|
||||
else
|
||||
LEFT="${GC}🎯 ${GOAL}${RESET}"
|
||||
fi
|
||||
|
||||
LINE0="${LEFT}"
|
||||
|
||||
# Historial: emojis de los ultimos 7 estados PREVIOS (sin el actual, que
|
||||
# se muestra completo a la derecha), atenuados y separados por │.
|
||||
PREV=$(jq -r '(.history // []) | .[0:-1] | .[-7:] | .[]' "$GOAL_FILE" 2>/dev/null)
|
||||
if [ -n "$PREV" ]; then
|
||||
HJOIN=""
|
||||
while IFS= read -r slug; do
|
||||
[ -z "$slug" ] && continue
|
||||
HS=$(phase_style "$slug")
|
||||
HIC="${HS%%|*}"
|
||||
HJOIN="${HJOIN}${HIC}"
|
||||
done <<< "$PREV"
|
||||
[ -n "$HJOIN" ] && LINE0="${LINE0} ${GRAY}│${RESET} ${DIM}${HJOIN}${RESET}"
|
||||
fi
|
||||
|
||||
# Fase actual (completa, con color e icono).
|
||||
if [ -n "$PHASE" ]; then
|
||||
PS=$(phase_style "$PHASE")
|
||||
PICON="${PS%%|*}"
|
||||
REST="${PS#*|}"
|
||||
PCOL="${REST%%|*}"
|
||||
PLABEL="${REST#*|}"
|
||||
LINE0="${LINE0} ${GRAY}│${RESET} \033[1;${PCOL}m${PICON} ${PLABEL}${RESET}"
|
||||
fi
|
||||
echo -e "$LINE0"
|
||||
# DoD en su propia linea debajo del objetivo, atenuado (🏁 = condicion de hecho).
|
||||
[ -n "$DOD" ] && echo -e " ${DIM}🏁 ${DOD}${RESET}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Imprimir resultado (2 líneas)
|
||||
echo -e "$LINE1"
|
||||
echo -e "$LINE2"
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
# Claude Code — Skills, Agents & Tools
|
||||
|
||||
Sistema de automatizacion para desarrollo de software usando Claude Code. Incluye skills (comandos invocables), agentes especializados, y herramientas Go para orquestar trabajo paralelo.
|
||||
|
||||
## Estructura del repo
|
||||
|
||||
```
|
||||
repo_Claude/
|
||||
├── bin/ # Binarios compilados
|
||||
│ └── parallel-executor # Orquestador de ejecucion paralela
|
||||
├── utils/
|
||||
│ └── parallel-executor/ # Codigo fuente Go del orquestador
|
||||
│ ├── core/ # Funciones puras (parser, planner)
|
||||
│ └── shell/ # I/O (worktrees, executor, logger)
|
||||
├── dev/
|
||||
│ └── issues/ # Sistema local de issues
|
||||
└── install.sh # Instalador
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skills
|
||||
|
||||
Skills son comandos invocables desde Claude Code con `/nombre`. Viven en `~/.claude/skills/`.
|
||||
|
||||
### Configuracion y setup
|
||||
|
||||
| Skill | Descripcion | Uso |
|
||||
|-------|-------------|-----|
|
||||
| `/primer` | Genera CLAUDE.md personalizado analizando el repo | `/primer` |
|
||||
| `/init-jupyter` | Inicializa proyecto Jupyter + MCP (bash script idempotente) | `/init-jupyter [ruta]` |
|
||||
| `/init-go-module` | Crea modulo Go funcional con bindings Python (CGO + ctypes) | `/init-go-module nombre` |
|
||||
| `/init-frontend` | Crea proyecto React/Vite o Wails desktop | `/init-frontend nombre [--wails]` |
|
||||
| `/nochanges` | Modo read-only para explorar sin modificar | `/nochanges` |
|
||||
| `/create-skill` | Crea un skill nuevo | `/create-skill nombre` |
|
||||
| `/create-agent` | Crea un agente especializado | `/create-agent` |
|
||||
|
||||
### Git
|
||||
|
||||
| Skill | Descripcion | Uso |
|
||||
|-------|-------------|-----|
|
||||
| `/git-branch` | Crea ramas `issue/*` o `quick/*` desde master | `/git-branch issue 0013 slug` |
|
||||
| `/git-push` | Commits atomicos por tipo, merge --no-ff, push, limpieza | `/git-push` |
|
||||
| `/git-recovery` | Recupera repo de estados inconsistentes | `/git-recovery [--aggressive]` |
|
||||
|
||||
### Workspace (Gitea + SQLite)
|
||||
|
||||
| Skill | Descripcion | Uso |
|
||||
|-------|-------------|-----|
|
||||
| `/create-repo` | Crea workspace en Gitea con rollback | `/create-repo` |
|
||||
| `/import-repo` | Importa repo existente a Gitea (mirror) | `/import-repo` |
|
||||
| `/sync-repos` | Sincroniza workspaces locales con Gitea | `/sync-repos [--dry-run]` |
|
||||
| `/list-repos` | Lista workspaces desde SQLite | `/list-repos [--filter x]` |
|
||||
| `/cleanup-worktrees` | Limpia worktrees post-merge | `/cleanup-worktrees [--all]` |
|
||||
|
||||
### Issues
|
||||
|
||||
| Skill | Descripcion | Uso |
|
||||
|-------|-------------|-----|
|
||||
| `/create-issue` | Crea issue con template, sub-issues si es grande | `/create-issue` |
|
||||
| `/fix-issue` | E2E: lee issue, branch, implementa, tests, merge | `/fix-issue 0013` |
|
||||
| `/auto-fix` | Igual que fix-issue pero sin confirmacion | `/auto-fix 0013` |
|
||||
| `/auto-create` | Igual que create-issue sin confirmacion | `/auto-create` |
|
||||
| `/quick-issue` | Issue minimal desde texto (para TUI) | `/quick-issue --text "..."` |
|
||||
| `/issues-status` | Dashboard con filtros por workspace/estado/tag | `/issues-status` |
|
||||
|
||||
### Ejecucion paralela
|
||||
|
||||
| Skill | Descripcion | Uso |
|
||||
|-------|-------------|-----|
|
||||
| `/sort-issues` | Analiza dependencias, topological sort | `/sort-issues` |
|
||||
| `/parallel-issues` | Genera plan de ejecucion paralela | `/parallel-issues` |
|
||||
| `/execute-parallel` | Crea worktrees y ejecuta issues en paralelo | `/execute-parallel [--dry-run]` |
|
||||
|
||||
---
|
||||
|
||||
## Agentes
|
||||
|
||||
Agentes especializados que Claude Code puede invocar automaticamente. Cada uno tiene su propio modelo, herramientas y MCP servers. Viven en `~/.claude/agents/`.
|
||||
|
||||
### Librerias de desarrollo
|
||||
|
||||
| Agente | Modelo | Descripcion | Ubicacion |
|
||||
|--------|--------|-------------|-----------|
|
||||
| **backend-lib** | sonnet | Gestiona DevFactory — libreria Go funcional con Result[T], Option[T], HTTP, DB, finance | `~/.local_agentes/backend` |
|
||||
| **frontend-lib** | sonnet | Gestiona Frontend_Library — 50+ componentes React/TS, temas OKLCH, shadcn/ui, charts, DSP | `~/.local_agentes/frontend` |
|
||||
|
||||
### Build y deploy
|
||||
|
||||
| Agente | Modelo | Descripcion |
|
||||
|--------|--------|-------------|
|
||||
| **build-wails** | sonnet | Apps desktop Wails v2 (Go + React). Cross-compile Linux/Windows/macOS. Integra ambas librerias |
|
||||
| **docker** | sonnet | Dockerfiles multi-stage, docker-compose, push a registries, deploy via SSH |
|
||||
|
||||
### Datos e infraestructura
|
||||
|
||||
| Agente | Modelo | Descripcion |
|
||||
|--------|--------|-------------|
|
||||
| **db-reader** | sonnet | Bases de datos SQLite y DuckDB. Consultas, imports CSV/Parquet/JSON, analisis OLAP |
|
||||
| **gitea** | sonnet | Gestion de Gitea: repos, issues, PRs, branches, archivos. MCP integrado |
|
||||
|
||||
### Automatizacion
|
||||
|
||||
| Agente | Modelo | Descripcion |
|
||||
|--------|--------|-------------|
|
||||
| **navegator** | sonnet | Automatizacion web con Go + Chrome DevTools Protocol (chromedp). Perfiles de navegacion |
|
||||
|
||||
---
|
||||
|
||||
## Parallel Executor
|
||||
|
||||
Binario Go en `utils/parallel-executor/` que orquesta la ejecucion paralela de issues usando git worktrees.
|
||||
|
||||
### Arquitectura
|
||||
|
||||
Sigue el patron **pure core / impure shell** de DevFactory:
|
||||
|
||||
```
|
||||
utils/parallel-executor/
|
||||
├── core/ # Funciones puras, 0 side effects
|
||||
│ ├── parser.go # Parsea PARALLEL_EXECUTION_ORDER.md
|
||||
│ ├── planner.go # Topological sort (Kahn), deteccion de ciclos
|
||||
│ ├── parser_test.go
|
||||
│ └── planner_test.go
|
||||
├── shell/ # Operaciones I/O con Result[T]
|
||||
│ ├── worktree.go # CRUD de git worktrees
|
||||
│ ├── executor.go # Invoca `claude -p` en cada worktree
|
||||
│ └── logger.go # Logs por sesion e issue
|
||||
├── main.go # CLI
|
||||
├── go.mod + go.work # DevFactory como dependencia
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
### Uso
|
||||
|
||||
```bash
|
||||
# Compilar
|
||||
cd utils/parallel-executor && make build
|
||||
|
||||
# Analizar issues y generar plan
|
||||
./bin/parallel-executor --sort
|
||||
|
||||
# Ver que haria sin ejecutar
|
||||
./bin/parallel-executor --dry-run
|
||||
|
||||
# Ejecutar todo
|
||||
./bin/parallel-executor
|
||||
|
||||
# Solo un grupo
|
||||
./bin/parallel-executor --group 1
|
||||
|
||||
# Sin paralelismo
|
||||
./bin/parallel-executor --sequential
|
||||
|
||||
# Solo limpiar worktrees
|
||||
./bin/parallel-executor --cleanup
|
||||
```
|
||||
|
||||
### Flujo de ejecucion
|
||||
|
||||
1. Lee (o genera) `PARALLEL_EXECUTION_ORDER.md`
|
||||
2. Agrupa issues por dependencias (topological sort)
|
||||
3. Por cada grupo, crea worktrees en `worktrees/issue-NNNN/`
|
||||
4. Ejecuta `claude -p` en paralelo dentro de cada worktree
|
||||
5. Mergea branches exitosas a master (`--no-ff`)
|
||||
6. Limpia worktrees automaticamente
|
||||
7. Escribe logs en `logs/`
|
||||
|
||||
---
|
||||
|
||||
## Stack tecnologico
|
||||
|
||||
### Backend (Go)
|
||||
|
||||
- **Go 1.22+** con generics
|
||||
- **DevFactory** (`~/.local_agentes/backend`): Result[T], Option[T], MapSlice, FilterSlice, Reduce, Pipe, Curry
|
||||
- Patron **pure core / impure shell**: funciones puras en `core/`, I/O wrapeado en Result[T] en `shell/`
|
||||
- **DuckDB** para analytics, **SQLite** para metadata
|
||||
- **Bubble Tea** para TUIs
|
||||
|
||||
### Frontend (React)
|
||||
|
||||
- **React 19** + TypeScript strict + **Vite** + **Tailwind CSS 4** (OKLCH)
|
||||
- **Frontend_Library** (`~/.local_agentes/frontend`): shadcn/ui, Phosphor Icons, 50+ componentes
|
||||
- **pnpm** exclusivamente, link via `pnpm add @anthropic/frontend-lib@link:...`
|
||||
- Vite dedupe obligatorio para `react`, `react-dom`
|
||||
- **Storybook 10** para documentacion de componentes
|
||||
|
||||
### Desktop
|
||||
|
||||
- **Wails v2** (Go backend + React frontend)
|
||||
- Cross-compile: Linux AMD64/ARM64, Windows AMD64
|
||||
- DevFactory via `go.work`, Frontend_Library via `pnpm link`
|
||||
|
||||
### Infraestructura
|
||||
|
||||
- **Gitea** self-hosted para repositorios
|
||||
- **Docker** multi-stage builds
|
||||
- **Trunk-based development**: ramas `issue/*` y `quick/*`, merge rapido a master
|
||||
|
||||
---
|
||||
|
||||
## Convenciones
|
||||
|
||||
- **Inmutabilidad**: no mutar datos, crear copias nuevas
|
||||
- **Result[T]** para errores, no `(T, error)` — permite encadenamiento monadico
|
||||
- **Commits atomicos**: agrupados por tipo (feat, fix, refactor, docs, chore, test)
|
||||
- **Issues**: formato 3-4 digitos, estado en markdown, sub-issues con sufijo letra
|
||||
- **Skills con bash scripts**: idempotentes, detectan estado existente, colores en output
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,116 @@
|
||||
# 0010 — Consolidar Skills de Issues en una Skill Unificada
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0010 |
|
||||
| **Estado** | 🟡 pendiente |
|
||||
| **Prioridad** | media |
|
||||
| **Tipo** | refactor |
|
||||
|
||||
## Dependencias
|
||||
|
||||
Ninguna.
|
||||
|
||||
**Desbloquea:** mejor mantenibilidad del sistema de skills
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Consolidar las 6 skills de gestión de issues (`create-issue`, `auto-create`, `quick-issue`, `fix-issue`, `auto-fix`, `issues-status`) en 2 skills unificadas con flags, reduciendo duplicación y simplificando el mantenimiento.
|
||||
|
||||
## Contexto
|
||||
|
||||
- Actualmente hay 3 variantes de creación (`create-issue`, `auto-create`, `quick-issue`) que difieren solo en el nivel de confirmación
|
||||
- Hay 2 variantes de implementación (`fix-issue`, `auto-fix`) que difieren solo en confirmación
|
||||
- Las 3 skills de análisis/ejecución paralela (`sort-issues`, `parallel-issues`, `execute-parallel`) son un pipeline secuencial que podría ser un solo flujo
|
||||
- Mantener 6+ skills con lógica casi idéntica genera drift y bugs silenciosos
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
skills/
|
||||
├── issue/SKILL.md — NUEVA: unifica create + auto-create + quick-issue
|
||||
├── fix/SKILL.md — NUEVA: unifica fix-issue + auto-fix
|
||||
├── issues-status/SKILL.md — SE MANTIENE (es diferente)
|
||||
├── parallel-pipeline/SKILL.md — NUEVA: unifica sort + parallel + execute
|
||||
│
|
||||
├── create-issue/ — DEPRECAR
|
||||
├── auto-create/ — DEPRECAR
|
||||
├── quick-issue/ — DEPRECAR
|
||||
├── fix-issue/ — DEPRECAR
|
||||
├── auto-fix/ — DEPRECAR
|
||||
├── sort-issues/ — DEPRECAR
|
||||
├── parallel-issues/ — DEPRECAR
|
||||
└── execute-parallel/ — DEPRECAR
|
||||
```
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1: Skill `/issue` unificada
|
||||
|
||||
- [ ] **1.1** Crear `skills/issue/SKILL.md` que acepte flags:
|
||||
- `/issue` → modo interactivo con confirmación (actual `create-issue`)
|
||||
- `/issue --auto` → sin confirmación (actual `auto-create`)
|
||||
- `/issue --quick --text "descripción"` → minimal desde TUI (actual `quick-issue`)
|
||||
- [ ] **1.2** Extraer lógica común: detección de número, slug, template, git integration
|
||||
- [ ] **1.3** Tests: verificar los 3 modos producen el mismo resultado
|
||||
|
||||
### Fase 2: Skill `/fix` unificada
|
||||
|
||||
- [ ] **2.1** Crear `skills/fix/SKILL.md` que acepte flags:
|
||||
- `/fix <NNNN>` → modo interactivo con confirmación (actual `fix-issue`)
|
||||
- `/fix <NNNN> --auto` → sin confirmación (actual `auto-fix`)
|
||||
- [ ] **2.2** Extraer lógica común: lectura de issue, branch, implementación, tests, cierre
|
||||
|
||||
### Fase 3: Skill `/parallel` unificada
|
||||
|
||||
- [ ] **3.1** Crear `skills/parallel-pipeline/SKILL.md` que combine:
|
||||
- Análisis de dependencias (actual `sort-issues`)
|
||||
- Generación de plan paralelo (actual `parallel-issues`)
|
||||
- Ejecución con worktrees (actual `execute-parallel`)
|
||||
- [ ] **3.2** Flags: `--dry-run`, `--group N`, `--sequential`, `--plan-only`
|
||||
|
||||
### Fase 4: Migración y limpieza
|
||||
|
||||
- [ ] **4.1** Mantener skills antiguas como aliases temporales (1 semana)
|
||||
- [ ] **4.2** Eliminar skills deprecadas
|
||||
- [ ] **4.3** Actualizar `skills/README.md`
|
||||
- [ ] **4.4** Actualizar documentación en issues que referencien las skills antiguas
|
||||
|
||||
## Ejemplo de uso
|
||||
|
||||
```bash
|
||||
# Antes (3 skills distintas):
|
||||
/create-issue
|
||||
/auto-create
|
||||
/quick-issue --text "bug en parser"
|
||||
|
||||
# Después (1 skill con flags):
|
||||
/issue
|
||||
/issue --auto
|
||||
/issue --quick --text "bug en parser"
|
||||
|
||||
# Antes (2 skills distintas):
|
||||
/fix-issue 0010
|
||||
/auto-fix 0010
|
||||
|
||||
# Después:
|
||||
/fix 0010
|
||||
/fix 0010 --auto
|
||||
```
|
||||
|
||||
## Decisiones de diseño
|
||||
|
||||
- **Flags sobre skills separadas**: Reduce de 8 skills a 3, misma funcionalidad. El flag `--auto` es más intuitivo que recordar `auto-fix` vs `fix-issue`
|
||||
- **`issues-status` se mantiene separada**: Es un dashboard de consulta, no comparte lógica con creación/implementación
|
||||
- **Pipeline paralelo como skill única**: sort → plan → execute es siempre secuencial, no tiene sentido invocarlos por separado
|
||||
|
||||
## Criterios de aceptación
|
||||
|
||||
- [ ] Las 3 nuevas skills cubren 100% de la funcionalidad de las 8 antiguas
|
||||
- [ ] Los flags son consistentes entre skills (`--auto`, `--dry-run`)
|
||||
- [ ] Skills antiguas eliminadas sin romper nada
|
||||
- [ ] README.md de skills actualizado
|
||||
@@ -21,6 +21,12 @@ Sistema local de issues para trackear mejoras y nuevos agentes.
|
||||
| 008 | [frontend-lib](008-improve-frontend-lib.md) - Versionado, testing | Media | Pendiente |
|
||||
| 009 | [gitea](009-improve-gitea.md) - Actions, templates | Media | Pendiente |
|
||||
|
||||
## Mejoras a Skills
|
||||
|
||||
| # | Issue | Prioridad | Estado |
|
||||
|---|-------|-----------|--------|
|
||||
| 010 | [consolidate-issue-skills](010-consolidate-issue-skills.md) - Unificar skills de issues con flags | Media | Pendiente |
|
||||
|
||||
## Agentes Completados
|
||||
|
||||
| Agente | Descripción | Fecha |
|
||||
|
||||
+165
-8
@@ -9,7 +9,7 @@ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CLAUDE_DIR="$HOME/.claude"
|
||||
|
||||
# Carpetas a enlazar (configuración compartible)
|
||||
FOLDERS=("skills" "agents")
|
||||
FOLDERS=("skills" "agents" "commands")
|
||||
|
||||
echo "=== Instalando configuración de Claude ==="
|
||||
echo "Repositorio: $REPO_DIR/.claude"
|
||||
@@ -54,20 +54,26 @@ done
|
||||
echo ""
|
||||
echo "=== Instalando archivos de configuración ==="
|
||||
|
||||
# 1. Status Line Script
|
||||
# 1. Status Line Script (enlace simbólico)
|
||||
STATUSLINE_SOURCE="$REPO_DIR/.claude/statusline.sh"
|
||||
STATUSLINE_TARGET="$CLAUDE_DIR/statusline.sh"
|
||||
|
||||
if [ -f "$STATUSLINE_SOURCE" ]; then
|
||||
if [ -f "$STATUSLINE_TARGET" ]; then
|
||||
chmod +x "$STATUSLINE_SOURCE"
|
||||
if [ -L "$STATUSLINE_TARGET" ] && [ "$(readlink "$STATUSLINE_TARGET")" = "$STATUSLINE_SOURCE" ]; then
|
||||
echo "OK: statusline.sh ya está enlazado correctamente"
|
||||
else
|
||||
# Symlink (roto o apuntando mal): borrar; archivo real: backup
|
||||
if [ -L "$STATUSLINE_TARGET" ]; then
|
||||
rm -f "$STATUSLINE_TARGET"
|
||||
elif [ -e "$STATUSLINE_TARGET" ]; then
|
||||
BACKUP="$STATUSLINE_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
echo "Backup: statusline.sh -> $BACKUP"
|
||||
mv "$STATUSLINE_TARGET" "$BACKUP"
|
||||
fi
|
||||
|
||||
cp "$STATUSLINE_SOURCE" "$STATUSLINE_TARGET"
|
||||
chmod +x "$STATUSLINE_TARGET"
|
||||
echo "Copiado: statusline.sh (ejecutable)"
|
||||
ln -s "$STATUSLINE_SOURCE" "$STATUSLINE_TARGET"
|
||||
echo "Enlazado: statusline.sh -> $STATUSLINE_SOURCE"
|
||||
fi
|
||||
else
|
||||
echo "WARN: statusline.sh no encontrado en el repo"
|
||||
fi
|
||||
@@ -96,13 +102,164 @@ else
|
||||
echo "WARN: settings.json no encontrado en el repo"
|
||||
fi
|
||||
|
||||
# 3. CLAUDE.md (enlace simbólico - preferencias globales)
|
||||
CLAUDEMD_SOURCE="$REPO_DIR/.claude/CLAUDE.md"
|
||||
CLAUDEMD_TARGET="$CLAUDE_DIR/CLAUDE.md"
|
||||
|
||||
if [ -f "$CLAUDEMD_SOURCE" ]; then
|
||||
if [ -L "$CLAUDEMD_TARGET" ] && [ "$(readlink "$CLAUDEMD_TARGET")" = "$CLAUDEMD_SOURCE" ]; then
|
||||
echo "OK: CLAUDE.md ya está enlazado correctamente"
|
||||
else
|
||||
# Symlink (roto o apuntando mal): borrar; archivo real: backup
|
||||
if [ -L "$CLAUDEMD_TARGET" ]; then
|
||||
rm -f "$CLAUDEMD_TARGET"
|
||||
elif [ -e "$CLAUDEMD_TARGET" ]; then
|
||||
BACKUP="$CLAUDEMD_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
echo "Backup: CLAUDE.md -> $BACKUP"
|
||||
mv "$CLAUDEMD_TARGET" "$BACKUP"
|
||||
fi
|
||||
ln -s "$CLAUDEMD_SOURCE" "$CLAUDEMD_TARGET"
|
||||
echo "Enlazado: CLAUDE.md -> $CLAUDEMD_SOURCE"
|
||||
fi
|
||||
else
|
||||
echo "WARN: CLAUDE.md no encontrado en el repo"
|
||||
fi
|
||||
|
||||
# === Instalando hooks (enlace simbólico por archivo) ===
|
||||
echo ""
|
||||
echo "=== Instalando hooks ==="
|
||||
|
||||
HOOKS_SOURCE_DIR="$REPO_DIR/.claude/hooks"
|
||||
HOOKS_TARGET_DIR="$CLAUDE_DIR/hooks"
|
||||
|
||||
if [ -d "$HOOKS_SOURCE_DIR" ]; then
|
||||
mkdir -p "$HOOKS_TARGET_DIR"
|
||||
for hook in "$HOOKS_SOURCE_DIR"/*.sh; do
|
||||
[ -e "$hook" ] || continue
|
||||
chmod +x "$hook"
|
||||
HOOK_NAME="$(basename "$hook")"
|
||||
HOOK_TARGET="$HOOKS_TARGET_DIR/$HOOK_NAME"
|
||||
|
||||
# Si ya es symlink correcto, saltar
|
||||
if [ -L "$HOOK_TARGET" ] && [ "$(readlink "$HOOK_TARGET")" = "$hook" ]; then
|
||||
echo "OK: hooks/$HOOK_NAME ya está enlazado correctamente"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Symlink (roto o apuntando mal): borrar sin backup; archivo real: backup
|
||||
if [ -L "$HOOK_TARGET" ]; then
|
||||
rm -f "$HOOK_TARGET"
|
||||
elif [ -e "$HOOK_TARGET" ]; then
|
||||
BACKUP="$HOOK_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
echo "Backup: hooks/$HOOK_NAME -> $BACKUP"
|
||||
mv "$HOOK_TARGET" "$BACKUP"
|
||||
fi
|
||||
|
||||
ln -s "$hook" "$HOOK_TARGET"
|
||||
echo "Enlazado: hooks/$HOOK_NAME -> $hook"
|
||||
done
|
||||
else
|
||||
echo "WARN: $HOOKS_SOURCE_DIR no existe, saltando hooks"
|
||||
fi
|
||||
|
||||
# === Limpieza de configuración que no debe cambiar ===
|
||||
echo ""
|
||||
echo "=== Limpiando configuración inmutable ==="
|
||||
|
||||
# 1. Eliminar backups viejos de settings.json (más de 7 días)
|
||||
DELETED_BACKUPS=0
|
||||
for backup in "$CLAUDE_DIR"/settings.json.backup.*; do
|
||||
[ -f "$backup" ] || continue
|
||||
if [ "$(find "$backup" -mtime +7 2>/dev/null)" ]; then
|
||||
rm -f "$backup"
|
||||
DELETED_BACKUPS=$((DELETED_BACKUPS + 1))
|
||||
fi
|
||||
done
|
||||
[ "$DELETED_BACKUPS" -gt 0 ] && echo "Eliminados $DELETED_BACKUPS backups viejos de settings.json"
|
||||
|
||||
# 2. Eliminar backups viejos de statusline.sh (más de 7 días)
|
||||
DELETED_SL=0
|
||||
for backup in "$CLAUDE_DIR"/statusline.sh.backup.*; do
|
||||
[ -f "$backup" ] || continue
|
||||
if [ "$(find "$backup" -mtime +7 2>/dev/null)" ]; then
|
||||
rm -f "$backup"
|
||||
DELETED_SL=$((DELETED_SL + 1))
|
||||
fi
|
||||
done
|
||||
[ "$DELETED_SL" -gt 0 ] && echo "Eliminados $DELETED_SL backups viejos de statusline.sh"
|
||||
|
||||
# 3. Si settings.json es un symlink correcto, eliminar cualquier settings.json suelto
|
||||
# que pueda haber quedado (no el symlink en sí)
|
||||
if [ -L "$CLAUDE_DIR/settings.json" ] && [ "$(readlink "$CLAUDE_DIR/settings.json")" = "$REPO_DIR/.claude/settings.json" ]; then
|
||||
# Eliminar archivos sueltos que puedan sobreescribir el symlink
|
||||
for stale in "$CLAUDE_DIR"/settings.json.tmp "$CLAUDE_DIR"/settings.json.new; do
|
||||
if [ -f "$stale" ]; then
|
||||
rm -f "$stale"
|
||||
echo "Eliminado archivo temporal: $(basename "$stale")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 4. Resetear settings.local.json a vacío si existe con contenido
|
||||
# (este archivo es para overrides locales temporales, no debe acumular config)
|
||||
LOCAL_SETTINGS="$CLAUDE_DIR/settings.local.json"
|
||||
if [ -f "$LOCAL_SETTINGS" ] && [ -s "$LOCAL_SETTINGS" ]; then
|
||||
CONTENT=$(cat "$LOCAL_SETTINGS" 2>/dev/null)
|
||||
# Solo limpiar si tiene contenido real (no solo {} o vacío)
|
||||
if [ "$CONTENT" != "{}" ] && [ "$CONTENT" != "" ]; then
|
||||
echo "WARN: settings.local.json tenía contenido, reseteando a vacío"
|
||||
echo -n "" > "$LOCAL_SETTINGS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 5. Asegurar que settings.json tiene los permisos de allow/deny correctos
|
||||
# Allow: editar .claude/ sin preguntar | Deny: nunca tocar .git/
|
||||
REQUIRED_PERMISSIONS='{
|
||||
"allow": [
|
||||
"Edit(~/.claude/**)",
|
||||
"Write(~/.claude/**)",
|
||||
"Edit(.claude/**)",
|
||||
"Write(.claude/**)"
|
||||
],
|
||||
"deny": [
|
||||
"Edit(~/.claude/.git/**)",
|
||||
"Write(~/.claude/.git/**)",
|
||||
"Edit(.git/**)",
|
||||
"Write(.git/**)"
|
||||
]
|
||||
}'
|
||||
|
||||
SETTINGS_FILE="$REPO_DIR/.claude/settings.json"
|
||||
if [ -f "$SETTINGS_FILE" ] && command -v jq &>/dev/null; then
|
||||
CURRENT_PERMS=$(jq -c '.permissions // empty' "$SETTINGS_FILE" 2>/dev/null)
|
||||
EXPECTED_PERMS=$(echo "$REQUIRED_PERMISSIONS" | jq -c '.')
|
||||
|
||||
if [ "$CURRENT_PERMS" != "$EXPECTED_PERMS" ]; then
|
||||
jq --argjson perms "$REQUIRED_PERMISSIONS" '.permissions = $perms' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" \
|
||||
&& mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
||||
echo "Actualizado: permissions en settings.json (allow .claude/*, deny .git/*)"
|
||||
else
|
||||
echo "OK: permissions ya están correctos"
|
||||
fi
|
||||
else
|
||||
echo "WARN: jq no disponible, no se pudo verificar permissions"
|
||||
fi
|
||||
|
||||
# 6. Asegurar que el settings.json del repo no tiene permisos de escritura para group/others
|
||||
chmod 644 "$REPO_DIR/.claude/settings.json"
|
||||
echo "Permisos de settings.json del repo: 644 (rw-r--r--)"
|
||||
|
||||
echo ""
|
||||
echo "=== Instalación completada ==="
|
||||
echo "Tus comandos y configuración ahora están sincronizados con el repositorio."
|
||||
echo ""
|
||||
echo "Configuración instalada:"
|
||||
echo " • Skills y Agents enlazados simbólicamente"
|
||||
echo " • Skills, Agents y Commands enlazados simbólicamente"
|
||||
echo " • Hooks (goal_*.sh) enlazados simbólicamente"
|
||||
echo " • CLAUDE.md (preferencias globales) enlazado"
|
||||
echo " • Status Line configurada con vibecoding setup"
|
||||
echo " • Settings.json enlazado (compartido entre repos)"
|
||||
echo " • Backups viejos limpiados (>7 días)"
|
||||
echo " • Archivos temporales de configuración eliminados"
|
||||
echo ""
|
||||
echo "Reinicia Claude Code para ver la nueva status line."
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.PHONY: build test clean install
|
||||
|
||||
BIN := parallel-executor
|
||||
BUILD_DIR := ../../bin
|
||||
|
||||
## build: Compila el binario
|
||||
build:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
go build -trimpath -ldflags="-s -w" -o $(BUILD_DIR)/$(BIN) .
|
||||
@echo "✓ Built $(BUILD_DIR)/$(BIN)"
|
||||
|
||||
## test: Ejecuta tests
|
||||
test:
|
||||
go test ./core/... -v
|
||||
|
||||
## clean: Elimina artefactos
|
||||
clean:
|
||||
rm -f $(BUILD_DIR)/$(BIN)
|
||||
|
||||
## install: Copia binario a la skill
|
||||
install: build
|
||||
@echo "✓ Binary at $(BUILD_DIR)/$(BIN)"
|
||||
@echo " Use: $(BUILD_DIR)/$(BIN) --help"
|
||||
|
||||
## tidy: go mod tidy
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
## help: Muestra ayuda
|
||||
help:
|
||||
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'
|
||||
@@ -0,0 +1,251 @@
|
||||
// Package core contiene funciones puras para el parallel executor.
|
||||
// Sin side effects: parseo de planes, análisis de dependencias, agrupamiento.
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
df "github.com/lucasdataproyects/devfactory/core"
|
||||
)
|
||||
|
||||
// Issue representa una issue parseada del plan de ejecución.
|
||||
type Issue struct {
|
||||
Number int
|
||||
Slug string
|
||||
Title string
|
||||
Group int
|
||||
Deps []int
|
||||
}
|
||||
|
||||
// ExecutionPlan es el plan completo parseado del markdown.
|
||||
type ExecutionPlan struct {
|
||||
Groups []IssueGroup
|
||||
Total int
|
||||
}
|
||||
|
||||
// IssueGroup es un conjunto de issues ejecutables en paralelo.
|
||||
type IssueGroup struct {
|
||||
Number int
|
||||
Name string
|
||||
Issues []Issue
|
||||
}
|
||||
|
||||
// WorktreeSpec define la especificación para crear un worktree.
|
||||
type WorktreeSpec struct {
|
||||
Issue Issue
|
||||
BranchName string
|
||||
WorkDir string
|
||||
}
|
||||
|
||||
// ExecutionResult es el resultado de ejecutar una issue.
|
||||
type ExecutionResult struct {
|
||||
Issue Issue
|
||||
Success bool
|
||||
Duration string
|
||||
Error string
|
||||
LogFile string
|
||||
}
|
||||
|
||||
var (
|
||||
groupHeaderRe = regexp.MustCompile(`^##\s+Grupo\s+(\d+)`)
|
||||
issueLineRe = regexp.MustCompile(`-\s+Issue\s+#(\d{4})\s*-?\s*(.*)`)
|
||||
depRe = regexp.MustCompile(`#(\d{4})`)
|
||||
)
|
||||
|
||||
// ParsePlan parsea el contenido markdown de PARALLEL_EXECUTION_ORDER.md.
|
||||
// Función pura: recibe string, retorna Result[ExecutionPlan].
|
||||
func ParsePlan(content string) df.Result[ExecutionPlan] {
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) == 0 {
|
||||
return df.Err[ExecutionPlan](errors.New("empty plan"))
|
||||
}
|
||||
|
||||
groups := parsePlanGroups(lines)
|
||||
if len(groups) == 0 {
|
||||
return df.Err[ExecutionPlan](errors.New("no groups found in plan"))
|
||||
}
|
||||
|
||||
total := df.Reduce(groups, 0, func(acc int, g IssueGroup) int {
|
||||
return acc + len(g.Issues)
|
||||
})
|
||||
|
||||
return df.Ok(ExecutionPlan{
|
||||
Groups: groups,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// parsePlanGroups extrae los grupos del markdown.
|
||||
func parsePlanGroups(lines []string) []IssueGroup {
|
||||
var groups []IssueGroup
|
||||
var currentGroup *IssueGroup
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if matches := groupHeaderRe.FindStringSubmatch(trimmed); len(matches) > 1 {
|
||||
if currentGroup != nil {
|
||||
groups = append(groups, *currentGroup)
|
||||
}
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
currentGroup = &IssueGroup{
|
||||
Number: num,
|
||||
Name: trimmed,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if currentGroup != nil {
|
||||
if matches := issueLineRe.FindStringSubmatch(trimmed); len(matches) > 1 {
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
title := strings.TrimSpace(matches[2])
|
||||
deps := extractDeps(title, num)
|
||||
issue := Issue{
|
||||
Number: num,
|
||||
Slug: issueSlug(num, title),
|
||||
Title: title,
|
||||
Group: currentGroup.Number,
|
||||
Deps: deps,
|
||||
}
|
||||
currentGroup.Issues = append(currentGroup.Issues, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentGroup != nil && len(currentGroup.Issues) > 0 {
|
||||
groups = append(groups, *currentGroup)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// extractDeps extrae números de issues referenciadas como dependencias.
|
||||
func extractDeps(text string, selfNum int) []int {
|
||||
matches := depRe.FindAllStringSubmatch(text, -1)
|
||||
return df.FilterSlice(
|
||||
df.MapSlice(matches, func(m []string) int {
|
||||
n, _ := strconv.Atoi(m[1])
|
||||
return n
|
||||
}),
|
||||
func(n int) bool { return n != selfNum },
|
||||
)
|
||||
}
|
||||
|
||||
// issueSlug genera el slug de branch para una issue.
|
||||
func issueSlug(num int, title string) string {
|
||||
slug := strings.ToLower(title)
|
||||
slug = regexp.MustCompile(`[^a-z0-9\s-]`).ReplaceAllString(slug, "")
|
||||
slug = regexp.MustCompile(`\s+`).ReplaceAllString(slug, "-")
|
||||
slug = regexp.MustCompile(`-{2,}`).ReplaceAllString(slug, "-")
|
||||
slug = strings.Trim(slug, "-")
|
||||
|
||||
words := strings.SplitN(slug, "-", 5)
|
||||
if len(words) > 4 {
|
||||
words = words[:4]
|
||||
}
|
||||
slug = strings.Join(words, "-")
|
||||
|
||||
if slug == "" {
|
||||
slug = "issue"
|
||||
}
|
||||
|
||||
return formatIssueNum(num) + "-" + slug
|
||||
}
|
||||
|
||||
func formatIssueNum(n int) string {
|
||||
return fmt.Sprintf("%04d", n)
|
||||
}
|
||||
|
||||
// FilterGroup filtra el plan para ejecutar solo un grupo específico.
|
||||
func FilterGroup(plan ExecutionPlan, groupNum int) df.Result[ExecutionPlan] {
|
||||
filtered := df.FilterSlice(plan.Groups, func(g IssueGroup) bool {
|
||||
return g.Number == groupNum
|
||||
})
|
||||
if len(filtered) == 0 {
|
||||
return df.Err[ExecutionPlan](fmt.Errorf("group not found: %d", groupNum))
|
||||
}
|
||||
total := len(filtered[0].Issues)
|
||||
return df.Ok(ExecutionPlan{Groups: filtered, Total: total})
|
||||
}
|
||||
|
||||
// BuildWorktreeSpecs genera las specs de worktrees para un grupo de issues.
|
||||
// Función pura: recibe issues y base path, retorna specs.
|
||||
func BuildWorktreeSpecs(issues []Issue, basePath string) []WorktreeSpec {
|
||||
return df.MapSlice(issues, func(issue Issue) WorktreeSpec {
|
||||
branch := "issue/" + issue.Slug
|
||||
workDir := basePath + "/worktrees/issue-" + formatIssueNum(issue.Number)
|
||||
return WorktreeSpec{
|
||||
Issue: issue,
|
||||
BranchName: branch,
|
||||
WorkDir: workDir,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ParseIssueFiles parsea los archivos de issues del directorio dev/issues/.
|
||||
// Retorna issues con dependencias extraídas del contenido de cada archivo.
|
||||
func ParseIssueFiles(files map[string]string) []Issue {
|
||||
var issues []Issue
|
||||
|
||||
numRe := regexp.MustCompile(`^(\d{3,4})[a-z]?-(.+)\.md$`)
|
||||
|
||||
for filename, content := range files {
|
||||
matches := numRe.FindStringSubmatch(filename)
|
||||
if len(matches) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(matches[1])
|
||||
slug := matches[2]
|
||||
|
||||
// Extraer título de la primera línea
|
||||
title := slug
|
||||
for _, line := range strings.SplitN(content, "\n", 5) {
|
||||
if strings.HasPrefix(line, "# ") {
|
||||
title = strings.TrimPrefix(line, "# ")
|
||||
title = strings.TrimSpace(title)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Extraer dependencias de "Bloqueada por" o tabla de dependencias
|
||||
deps := extractAllDeps(content, num)
|
||||
|
||||
// Solo incluir si está pendiente
|
||||
if isIssuePending(content) {
|
||||
issues = append(issues, Issue{
|
||||
Number: num,
|
||||
Slug: slug,
|
||||
Title: title,
|
||||
Deps: deps,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
func extractAllDeps(content string, selfNum int) []int {
|
||||
allMatches := depRe.FindAllStringSubmatch(content, -1)
|
||||
seen := make(map[int]bool)
|
||||
var deps []int
|
||||
for _, m := range allMatches {
|
||||
n, _ := strconv.Atoi(m[1])
|
||||
if n != selfNum && !seen[n] {
|
||||
seen[n] = true
|
||||
deps = append(deps, n)
|
||||
}
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
func isIssuePending(content string) bool {
|
||||
lower := strings.ToLower(content)
|
||||
return strings.Contains(lower, "pendiente") ||
|
||||
strings.Contains(lower, "🟡") ||
|
||||
(!strings.Contains(lower, "completado") && !strings.Contains(lower, "✅"))
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePlan(t *testing.T) {
|
||||
plan := `# Plan de Ejecución Paralela
|
||||
|
||||
## Grupo 1
|
||||
|
||||
- Issue #0003 - testing agent
|
||||
- Issue #0006 - improve db-reader
|
||||
|
||||
## Grupo 2
|
||||
|
||||
- Issue #0004 - api client — depende de #0003
|
||||
`
|
||||
|
||||
result := ParsePlan(plan)
|
||||
if result.IsErr() {
|
||||
t.Fatalf("ParsePlan failed: %v", result.Error())
|
||||
}
|
||||
|
||||
ep := result.Unwrap()
|
||||
if len(ep.Groups) != 2 {
|
||||
t.Errorf("expected 2 groups, got %d", len(ep.Groups))
|
||||
}
|
||||
if ep.Total != 3 {
|
||||
t.Errorf("expected 3 total issues, got %d", ep.Total)
|
||||
}
|
||||
if ep.Groups[0].Issues[0].Number != 3 {
|
||||
t.Errorf("expected issue #0003, got #%04d", ep.Groups[0].Issues[0].Number)
|
||||
}
|
||||
if len(ep.Groups[1].Issues[0].Deps) != 1 || ep.Groups[1].Issues[0].Deps[0] != 3 {
|
||||
t.Errorf("expected issue #0004 to depend on #0003")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePlanEmpty(t *testing.T) {
|
||||
result := ParsePlan("")
|
||||
if !result.IsErr() {
|
||||
t.Error("expected error for empty plan")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterGroup(t *testing.T) {
|
||||
plan := ExecutionPlan{
|
||||
Groups: []IssueGroup{
|
||||
{Number: 1, Issues: []Issue{{Number: 1}}},
|
||||
{Number: 2, Issues: []Issue{{Number: 2}, {Number: 3}}},
|
||||
},
|
||||
Total: 3,
|
||||
}
|
||||
|
||||
result := FilterGroup(plan, 2)
|
||||
if result.IsErr() {
|
||||
t.Fatalf("FilterGroup failed: %v", result.Error())
|
||||
}
|
||||
filtered := result.Unwrap()
|
||||
if filtered.Total != 2 {
|
||||
t.Errorf("expected 2 issues, got %d", filtered.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterGroupNotFound(t *testing.T) {
|
||||
plan := ExecutionPlan{Groups: []IssueGroup{{Number: 1}}}
|
||||
result := FilterGroup(plan, 99)
|
||||
if !result.IsErr() {
|
||||
t.Error("expected error for non-existent group")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWorktreeSpecs(t *testing.T) {
|
||||
issues := []Issue{
|
||||
{Number: 3, Slug: "0003-testing"},
|
||||
{Number: 6, Slug: "0006-db-reader"},
|
||||
}
|
||||
specs := BuildWorktreeSpecs(issues, "/tmp/project")
|
||||
if len(specs) != 2 {
|
||||
t.Fatalf("expected 2 specs, got %d", len(specs))
|
||||
}
|
||||
if specs[0].BranchName != "issue/0003-testing" {
|
||||
t.Errorf("expected branch issue/0003-testing, got %s", specs[0].BranchName)
|
||||
}
|
||||
if specs[1].WorkDir != "/tmp/project/worktrees/issue-0006" {
|
||||
t.Errorf("unexpected workdir: %s", specs[1].WorkDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
df "github.com/lucasdataproyects/devfactory/core"
|
||||
)
|
||||
|
||||
// TopologicalSort ordena issues por dependencias usando Kahn's algorithm.
|
||||
// Función pura: retorna Result con los grupos ordenados.
|
||||
func TopologicalSort(issues []Issue) df.Result[[]IssueGroup] {
|
||||
if len(issues) == 0 {
|
||||
return df.Err[[]IssueGroup](errors.New("no issues to sort"))
|
||||
}
|
||||
|
||||
// Construir mapa de issues por número
|
||||
issueMap := make(map[int]Issue)
|
||||
for _, issue := range issues {
|
||||
issueMap[issue.Number] = issue
|
||||
}
|
||||
|
||||
// Calcular in-degree (solo deps que existen en el set)
|
||||
inDegree := make(map[int]int)
|
||||
for _, issue := range issues {
|
||||
if _, exists := inDegree[issue.Number]; !exists {
|
||||
inDegree[issue.Number] = 0
|
||||
}
|
||||
for _, dep := range issue.Deps {
|
||||
if _, exists := issueMap[dep]; exists {
|
||||
inDegree[issue.Number]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kahn's algorithm por niveles (cada nivel = un grupo paralelo)
|
||||
var groups []IssueGroup
|
||||
resolved := make(map[int]bool)
|
||||
remaining := len(issues)
|
||||
groupNum := 1
|
||||
|
||||
for remaining > 0 {
|
||||
// Encontrar issues con in-degree 0
|
||||
var ready []Issue
|
||||
for _, issue := range issues {
|
||||
if !resolved[issue.Number] && inDegree[issue.Number] == 0 {
|
||||
issue.Group = groupNum
|
||||
ready = append(ready, issue)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ready) == 0 {
|
||||
// Ciclo detectado
|
||||
var cycleNums []int
|
||||
for _, issue := range issues {
|
||||
if !resolved[issue.Number] {
|
||||
cycleNums = append(cycleNums, issue.Number)
|
||||
}
|
||||
}
|
||||
return df.Err[[]IssueGroup](
|
||||
fmt.Errorf("circular dependency detected among issues: %v", cycleNums),
|
||||
)
|
||||
}
|
||||
|
||||
// Ordenar por número dentro del grupo (determinista)
|
||||
sort.Slice(ready, func(i, j int) bool {
|
||||
return ready[i].Number < ready[j].Number
|
||||
})
|
||||
|
||||
groups = append(groups, IssueGroup{
|
||||
Number: groupNum,
|
||||
Name: fmt.Sprintf("Grupo %d", groupNum),
|
||||
Issues: ready,
|
||||
})
|
||||
|
||||
// Marcar como resueltas y decrementar in-degree
|
||||
for _, issue := range ready {
|
||||
resolved[issue.Number] = true
|
||||
remaining--
|
||||
// Decrementar in-degree de dependientes
|
||||
for _, other := range issues {
|
||||
if !resolved[other.Number] {
|
||||
for _, dep := range other.Deps {
|
||||
if dep == issue.Number {
|
||||
inDegree[other.Number]--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupNum++
|
||||
}
|
||||
|
||||
return df.Ok(groups)
|
||||
}
|
||||
|
||||
// AnalyzeFileConflicts detecta issues que tocan los mismos archivos.
|
||||
// Recibe un mapa issue_number -> lista de archivos mencionados.
|
||||
// Retorna pares de issues en conflicto.
|
||||
func AnalyzeFileConflicts(filesByIssue map[int][]string) []ConflictPair {
|
||||
var conflicts []ConflictPair
|
||||
nums := sortedKeys(filesByIssue)
|
||||
|
||||
for i := 0; i < len(nums); i++ {
|
||||
for j := i + 1; j < len(nums); j++ {
|
||||
shared := intersect(filesByIssue[nums[i]], filesByIssue[nums[j]])
|
||||
if len(shared) > 0 {
|
||||
conflicts = append(conflicts, ConflictPair{
|
||||
IssueA: nums[i],
|
||||
IssueB: nums[j],
|
||||
SharedFiles: shared,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts
|
||||
}
|
||||
|
||||
// ConflictPair representa dos issues que comparten archivos.
|
||||
type ConflictPair struct {
|
||||
IssueA int
|
||||
IssueB int
|
||||
SharedFiles []string
|
||||
}
|
||||
|
||||
// GeneratePlanMarkdown genera el markdown del plan de ejecución.
|
||||
// Función pura: recibe grupos, retorna string.
|
||||
func GeneratePlanMarkdown(groups []IssueGroup, conflicts []ConflictPair) string {
|
||||
var b []byte
|
||||
|
||||
b = append(b, "# Plan de Ejecución Paralela\n\n"...)
|
||||
b = append(b, fmt.Sprintf("Generado automáticamente. Total: %d issues en %d grupos.\n\n",
|
||||
countIssues(groups), len(groups))...)
|
||||
|
||||
if len(conflicts) > 0 {
|
||||
b = append(b, "## Conflictos Detectados\n\n"...)
|
||||
for _, c := range conflicts {
|
||||
b = append(b, fmt.Sprintf("- Issue #%04d y #%04d comparten: %v\n",
|
||||
c.IssueA, c.IssueB, c.SharedFiles)...)
|
||||
}
|
||||
b = append(b, "\n"...)
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
b = append(b, fmt.Sprintf("## Grupo %d\n\n", group.Number)...)
|
||||
for _, issue := range group.Issues {
|
||||
depStr := ""
|
||||
if len(issue.Deps) > 0 {
|
||||
depStr = fmt.Sprintf(" — depende de %s", formatDeps(issue.Deps))
|
||||
}
|
||||
b = append(b, fmt.Sprintf("- Issue #%04d - %s%s\n", issue.Number, issue.Title, depStr)...)
|
||||
}
|
||||
b = append(b, "\n"...)
|
||||
}
|
||||
|
||||
b = append(b, "## Resumen\n\n"...)
|
||||
b = append(b, "| Métrica | Valor |\n"...)
|
||||
b = append(b, "|---------|-------|\n"...)
|
||||
b = append(b, fmt.Sprintf("| Issues totales | %d |\n", countIssues(groups))...)
|
||||
b = append(b, fmt.Sprintf("| Grupos paralelos | %d |\n", len(groups))...)
|
||||
if len(groups) > 0 {
|
||||
sequential := countIssues(groups)
|
||||
parallel := len(groups)
|
||||
if sequential > 0 {
|
||||
saving := ((sequential - parallel) * 100) / sequential
|
||||
b = append(b, fmt.Sprintf("| Ahorro estimado | %d%% |\n", saving)...)
|
||||
}
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func countIssues(groups []IssueGroup) int {
|
||||
return df.Reduce(groups, 0, func(acc int, g IssueGroup) int {
|
||||
return acc + len(g.Issues)
|
||||
})
|
||||
}
|
||||
|
||||
func formatDeps(deps []int) string {
|
||||
strs := df.MapSlice(deps, func(n int) string {
|
||||
return fmt.Sprintf("#%04d", n)
|
||||
})
|
||||
s := ""
|
||||
for i, str := range strs {
|
||||
if i > 0 {
|
||||
s += ", "
|
||||
}
|
||||
s += str
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func sortedKeys(m map[int][]string) []int {
|
||||
keys := make([]int, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func intersect(a, b []string) []string {
|
||||
set := make(map[string]bool)
|
||||
for _, s := range a {
|
||||
set[s] = true
|
||||
}
|
||||
var result []string
|
||||
for _, s := range b {
|
||||
if set[s] {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTopologicalSort(t *testing.T) {
|
||||
issues := []Issue{
|
||||
{Number: 1, Title: "base", Deps: nil},
|
||||
{Number: 2, Title: "depends on 1", Deps: []int{1}},
|
||||
{Number: 3, Title: "independent", Deps: nil},
|
||||
{Number: 4, Title: "depends on 1 and 3", Deps: []int{1, 3}},
|
||||
}
|
||||
|
||||
result := TopologicalSort(issues)
|
||||
if result.IsErr() {
|
||||
t.Fatalf("TopologicalSort failed: %v", result.Error())
|
||||
}
|
||||
|
||||
groups := result.Unwrap()
|
||||
if len(groups) < 2 {
|
||||
t.Fatalf("expected at least 2 groups, got %d", len(groups))
|
||||
}
|
||||
|
||||
// Group 1 should have issues 1 and 3 (no deps)
|
||||
g1Nums := make(map[int]bool)
|
||||
for _, issue := range groups[0].Issues {
|
||||
g1Nums[issue.Number] = true
|
||||
}
|
||||
if !g1Nums[1] || !g1Nums[3] {
|
||||
t.Errorf("group 1 should contain issues 1 and 3, got %v", groups[0].Issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopologicalSortCycle(t *testing.T) {
|
||||
issues := []Issue{
|
||||
{Number: 1, Title: "a", Deps: []int{2}},
|
||||
{Number: 2, Title: "b", Deps: []int{1}},
|
||||
}
|
||||
|
||||
result := TopologicalSort(issues)
|
||||
if !result.IsErr() {
|
||||
t.Error("expected cycle detection error")
|
||||
}
|
||||
if !strings.Contains(result.Error().Error(), "circular") {
|
||||
t.Errorf("expected circular dependency error, got: %v", result.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopologicalSortEmpty(t *testing.T) {
|
||||
result := TopologicalSort(nil)
|
||||
if !result.IsErr() {
|
||||
t.Error("expected error for empty issues")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeFileConflicts(t *testing.T) {
|
||||
files := map[int][]string{
|
||||
1: {"core/types.go", "shell/http.go"},
|
||||
2: {"core/types.go", "app/main.go"},
|
||||
3: {"app/other.go"},
|
||||
}
|
||||
|
||||
conflicts := AnalyzeFileConflicts(files)
|
||||
if len(conflicts) != 1 {
|
||||
t.Fatalf("expected 1 conflict, got %d", len(conflicts))
|
||||
}
|
||||
if conflicts[0].IssueA != 1 || conflicts[0].IssueB != 2 {
|
||||
t.Errorf("expected conflict between 1 and 2, got %d and %d",
|
||||
conflicts[0].IssueA, conflicts[0].IssueB)
|
||||
}
|
||||
if conflicts[0].SharedFiles[0] != "core/types.go" {
|
||||
t.Errorf("expected shared file core/types.go, got %s", conflicts[0].SharedFiles[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePlanMarkdown(t *testing.T) {
|
||||
groups := []IssueGroup{
|
||||
{Number: 1, Issues: []Issue{
|
||||
{Number: 1, Title: "base"},
|
||||
{Number: 3, Title: "other"},
|
||||
}},
|
||||
{Number: 2, Issues: []Issue{
|
||||
{Number: 2, Title: "depends", Deps: []int{1}},
|
||||
}},
|
||||
}
|
||||
|
||||
md := GeneratePlanMarkdown(groups, nil)
|
||||
if !strings.Contains(md, "## Grupo 1") {
|
||||
t.Error("markdown should contain group headers")
|
||||
}
|
||||
if !strings.Contains(md, "Issue #0001") {
|
||||
t.Error("markdown should contain issue references")
|
||||
}
|
||||
if !strings.Contains(md, "3 issues") || !strings.Contains(md, "2 grupos") {
|
||||
// Check summary table
|
||||
if !strings.Contains(md, "| 3 |") {
|
||||
t.Error("markdown should contain correct totals")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module github.com/lucasdataproyects/parallel-executor
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require github.com/lucasdataproyects/devfactory v0.0.0
|
||||
|
||||
require (
|
||||
github.com/apache/arrow/go/v14 v14.0.2 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/flatbuffers v23.5.26+incompatible // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/marcboeker/go-duckdb v1.6.5 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
golang.org/x/mod v0.13.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/tools v0.14.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/lucasdataproyects/devfactory => /home/lucas/.local_agentes/backend
|
||||
@@ -0,0 +1,45 @@
|
||||
github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO5IONw=
|
||||
github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/marcboeker/go-duckdb v1.6.5 h1:XCfR1JVZxsemcSPxRQKK0R0ESfgRMHTEqh3Y+dv40SI=
|
||||
github.com/marcboeker/go-duckdb v1.6.5/go.mod h1:WtWeqqhZoTke/Nbd7V9lnBx7I2/A/q0SAq/urGzPCMs=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o=
|
||||
gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,6 @@
|
||||
go 1.22.2
|
||||
|
||||
use (
|
||||
.
|
||||
/home/lucas/.local_agentes/backend
|
||||
)
|
||||
@@ -0,0 +1,352 @@
|
||||
// parallel-executor — Orquestador de ejecución paralela de issues.
|
||||
//
|
||||
// Crea git worktrees, ejecuta claude en cada uno, y mergea los resultados.
|
||||
// Usa DevFactory para Result[T], MapSlice, y operaciones I/O.
|
||||
//
|
||||
// Uso:
|
||||
//
|
||||
// parallel-executor [flags]
|
||||
// --plan <file> Plan markdown (default: PARALLEL_EXECUTION_ORDER.md)
|
||||
// --group <N> Ejecutar solo grupo N
|
||||
// --sequential Sin paralelismo
|
||||
// --dry-run Solo mostrar plan sin ejecutar
|
||||
// --timeout <min> Timeout por issue en minutos (default: 30)
|
||||
// --cleanup Solo limpiar worktrees existentes
|
||||
// --sort Analizar issues y generar plan (no ejecutar)
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dfshell "github.com/lucasdataproyects/devfactory/shell"
|
||||
|
||||
pcore "github.com/lucasdataproyects/parallel-executor/core"
|
||||
"github.com/lucasdataproyects/parallel-executor/shell"
|
||||
)
|
||||
|
||||
// CLI colors
|
||||
const (
|
||||
red = "\033[0;31m"
|
||||
green = "\033[0;32m"
|
||||
yellow = "\033[1;33m"
|
||||
blue = "\033[0;34m"
|
||||
cyan = "\033[0;36m"
|
||||
nc = "\033[0m"
|
||||
)
|
||||
|
||||
func main() {
|
||||
planFile := flag.String("plan", "PARALLEL_EXECUTION_ORDER.md", "plan markdown file")
|
||||
groupNum := flag.Int("group", 0, "execute only this group number")
|
||||
sequential := flag.Bool("sequential", false, "disable parallel execution")
|
||||
dryRun := flag.Bool("dry-run", false, "show plan without executing")
|
||||
timeoutMin := flag.Int("timeout", 30, "timeout per issue in minutes")
|
||||
cleanup := flag.Bool("cleanup", false, "only cleanup existing worktrees")
|
||||
sortMode := flag.Bool("sort", false, "analyze issues and generate plan")
|
||||
flag.Parse()
|
||||
|
||||
repoRoot, err := os.Getwd()
|
||||
if err != nil {
|
||||
fatal("cannot get working directory: %v", err)
|
||||
}
|
||||
|
||||
// --- Modo cleanup ---
|
||||
if *cleanup {
|
||||
info("Cleaning up worktrees...")
|
||||
result := shell.CleanupAllWorktrees(repoRoot)
|
||||
if result.IsErr() {
|
||||
fatal("cleanup failed: %v", result.Error())
|
||||
}
|
||||
ok("Cleaned %d worktrees", result.Unwrap())
|
||||
return
|
||||
}
|
||||
|
||||
// --- Modo sort: analizar issues y generar plan ---
|
||||
if *sortMode {
|
||||
generatePlan(repoRoot)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Ejecutar plan ---
|
||||
executePlan(repoRoot, *planFile, *groupNum, *sequential, *dryRun, *timeoutMin)
|
||||
}
|
||||
|
||||
func generatePlan(repoRoot string) {
|
||||
issuesDir := repoRoot + "/dev/issues"
|
||||
if !dfshell.DirExists(issuesDir) {
|
||||
fatal("issues directory not found: %s", issuesDir)
|
||||
}
|
||||
|
||||
info("Reading issues from %s...", issuesDir)
|
||||
filesResult := shell.ReadIssueFiles(issuesDir)
|
||||
if filesResult.IsErr() {
|
||||
fatal("cannot read issues: %v", filesResult.Error())
|
||||
}
|
||||
|
||||
issues := pcore.ParseIssueFiles(filesResult.Unwrap())
|
||||
if len(issues) == 0 {
|
||||
warn("No pending issues found")
|
||||
return
|
||||
}
|
||||
info("Found %d pending issues", len(issues))
|
||||
|
||||
// Topological sort
|
||||
groupsResult := pcore.TopologicalSort(issues)
|
||||
if groupsResult.IsErr() {
|
||||
fatal("dependency analysis failed: %v", groupsResult.Error())
|
||||
}
|
||||
|
||||
groups := groupsResult.Unwrap()
|
||||
|
||||
// Generate markdown
|
||||
markdown := pcore.GeneratePlanMarkdown(groups, nil)
|
||||
|
||||
outFile := repoRoot + "/PARALLEL_EXECUTION_ORDER.md"
|
||||
writeResult := dfshell.WriteString(outFile, markdown)
|
||||
if writeResult.IsErr() {
|
||||
fatal("cannot write plan: %v", writeResult.Error())
|
||||
}
|
||||
|
||||
ok("Plan generated: %s", outFile)
|
||||
fmt.Printf("\n%sIssues:%s %d\n", cyan, nc, len(issues))
|
||||
fmt.Printf("%sGroups:%s %d\n", cyan, nc, len(groups))
|
||||
for _, g := range groups {
|
||||
fmt.Printf(" %sGrupo %d:%s %d issues\n", blue, g.Number, nc, len(g.Issues))
|
||||
for _, issue := range g.Issues {
|
||||
fmt.Printf(" - #%04d %s\n", issue.Number, issue.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func executePlan(repoRoot string, planFile string, groupNum int, sequential bool, dryRun bool, timeoutMin int) {
|
||||
// Leer plan
|
||||
planContent := dfshell.ReadString(planFile)
|
||||
if planContent.IsErr() {
|
||||
// Intentar generar plan automáticamente
|
||||
warn("Plan not found, generating from issues...")
|
||||
generatePlan(repoRoot)
|
||||
planContent = dfshell.ReadString(planFile)
|
||||
if planContent.IsErr() {
|
||||
fatal("cannot read plan: %v", planContent.Error())
|
||||
}
|
||||
}
|
||||
|
||||
planResult := pcore.ParsePlan(planContent.Unwrap())
|
||||
if planResult.IsErr() {
|
||||
fatal("cannot parse plan: %v", planResult.Error())
|
||||
}
|
||||
|
||||
plan := planResult.Unwrap()
|
||||
|
||||
// Filtrar por grupo si se especificó
|
||||
if groupNum > 0 {
|
||||
filtered := pcore.FilterGroup(plan, groupNum)
|
||||
if filtered.IsErr() {
|
||||
fatal("group filter: %v", filtered.Error())
|
||||
}
|
||||
plan = filtered.Unwrap()
|
||||
}
|
||||
|
||||
info("Plan: %d issues in %d groups", plan.Total, len(plan.Groups))
|
||||
|
||||
// --- Dry run: solo mostrar ---
|
||||
if dryRun {
|
||||
for _, group := range plan.Groups {
|
||||
fmt.Printf("\n%s%s%s (%d issues)\n", cyan, group.Name, nc, len(group.Issues))
|
||||
specs := pcore.BuildWorktreeSpecs(group.Issues, repoRoot)
|
||||
for _, spec := range specs {
|
||||
fmt.Printf(" #%04d → %s\n", spec.Issue.Number, spec.BranchName)
|
||||
fmt.Printf(" %s\n", spec.WorkDir)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n%sDry run complete. No worktrees created.%s\n", yellow, nc)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Logger ---
|
||||
loggerResult := shell.NewLogger(repoRoot)
|
||||
if loggerResult.IsErr() {
|
||||
fatal("cannot create logger: %v", loggerResult.Error())
|
||||
}
|
||||
logger := loggerResult.Unwrap()
|
||||
info("Logs: %s", logger.SessionLogFile())
|
||||
|
||||
// --- Ejecutar grupos secuencialmente, issues dentro de cada grupo en paralelo ---
|
||||
timeout := time.Duration(timeoutMin) * time.Minute
|
||||
var allResults []pcore.ExecutionResult
|
||||
|
||||
for _, group := range plan.Groups {
|
||||
fmt.Printf("\n%s══════════════════════════════════════%s\n", cyan, nc)
|
||||
fmt.Printf("%s %s — %d issues%s\n", cyan, group.Name, len(group.Issues), nc)
|
||||
fmt.Printf("%s══════════════════════════════════════%s\n", cyan, nc)
|
||||
|
||||
specs := pcore.BuildWorktreeSpecs(group.Issues, repoRoot)
|
||||
|
||||
// Crear worktrees
|
||||
info("Creating %d worktrees...", len(specs))
|
||||
var validSpecs []pcore.WorktreeSpec
|
||||
for _, spec := range specs {
|
||||
result := shell.CreateWorktree(spec, repoRoot)
|
||||
if result.IsErr() {
|
||||
warn("Failed to create worktree for #%04d: %v", spec.Issue.Number, result.Error())
|
||||
allResults = append(allResults, pcore.ExecutionResult{
|
||||
Issue: spec.Issue,
|
||||
Success: false,
|
||||
Error: result.Error().Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
ok("Worktree: %s → %s", spec.BranchName, result.Unwrap())
|
||||
validSpecs = append(validSpecs, spec)
|
||||
}
|
||||
|
||||
// Ejecutar
|
||||
var groupResults []pcore.ExecutionResult
|
||||
if sequential || len(validSpecs) == 1 {
|
||||
groupResults = executeSequential(validSpecs, timeout, logger)
|
||||
} else {
|
||||
groupResults = executeParallel(validSpecs, timeout, logger)
|
||||
}
|
||||
|
||||
allResults = append(allResults, groupResults...)
|
||||
|
||||
// Mergear las exitosas
|
||||
for _, r := range groupResults {
|
||||
if !r.Success {
|
||||
continue
|
||||
}
|
||||
spec := findSpec(validSpecs, r.Issue.Number)
|
||||
if spec == nil {
|
||||
continue
|
||||
}
|
||||
info("Merging %s...", spec.BranchName)
|
||||
mergeResult := shell.MergeBranchToMaster(spec.BranchName, repoRoot)
|
||||
if mergeResult.IsErr() {
|
||||
warn("Merge failed for %s: %v", spec.BranchName, mergeResult.Error())
|
||||
} else {
|
||||
ok("Merged %s", spec.BranchName)
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar worktrees del grupo
|
||||
for _, spec := range validSpecs {
|
||||
shell.RemoveWorktree(spec, repoRoot)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Resumen ---
|
||||
summaryResult := logger.WriteSummary(allResults)
|
||||
summaryFile := ""
|
||||
if summaryResult.IsOk() {
|
||||
summaryFile = summaryResult.Unwrap()
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s══════════════════════════════════════%s\n", green, nc)
|
||||
fmt.Printf("%s Execution Complete%s\n", green, nc)
|
||||
fmt.Printf("%s══════════════════════════════════════%s\n\n", green, nc)
|
||||
|
||||
succeeded := 0
|
||||
failed := 0
|
||||
for _, r := range allResults {
|
||||
if r.Success {
|
||||
succeeded++
|
||||
fmt.Printf(" %s✓%s #%04d %s (%s)\n", green, nc, r.Issue.Number, r.Issue.Title, r.Duration)
|
||||
} else {
|
||||
failed++
|
||||
fmt.Printf(" %s✗%s #%04d %s — %s\n", red, nc, r.Issue.Number, r.Issue.Title, r.Error)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n Total: %d | Succeeded: %s%d%s | Failed: %s%d%s\n",
|
||||
len(allResults), green, succeeded, nc, red, failed, nc)
|
||||
if summaryFile != "" {
|
||||
fmt.Printf(" Summary: %s\n", summaryFile)
|
||||
}
|
||||
fmt.Printf(" Log: %s\n", logger.SessionLogFile())
|
||||
|
||||
// Cleanup final
|
||||
shell.CleanupAllWorktrees(repoRoot)
|
||||
|
||||
if failed > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func executeSequential(specs []pcore.WorktreeSpec, timeout time.Duration, logger *shell.Logger) []pcore.ExecutionResult {
|
||||
results := make([]pcore.ExecutionResult, 0, len(specs))
|
||||
|
||||
for _, spec := range specs {
|
||||
info("Executing #%04d %s...", spec.Issue.Number, spec.Issue.Title)
|
||||
logger.LogIssueStart(spec.Issue)
|
||||
|
||||
result := shell.ExecuteIssue(spec, timeout)
|
||||
logger.LogIssueResult(result)
|
||||
|
||||
if result.Success {
|
||||
ok("#%04d completed in %s", spec.Issue.Number, result.Duration)
|
||||
} else {
|
||||
warn("#%04d failed: %s", spec.Issue.Number, result.Error)
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func executeParallel(specs []pcore.WorktreeSpec, timeout time.Duration, logger *shell.Logger) []pcore.ExecutionResult {
|
||||
results := make([]pcore.ExecutionResult, len(specs))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i, spec := range specs {
|
||||
wg.Add(1)
|
||||
go func(idx int, s pcore.WorktreeSpec) {
|
||||
defer wg.Done()
|
||||
|
||||
info("[goroutine] Executing #%04d %s...", s.Issue.Number, s.Issue.Title)
|
||||
logger.LogIssueStart(s.Issue)
|
||||
|
||||
result := shell.ExecuteIssue(s, timeout)
|
||||
logger.LogIssueResult(result)
|
||||
results[idx] = result
|
||||
|
||||
if result.Success {
|
||||
ok("[goroutine] #%04d completed in %s", s.Issue.Number, result.Duration)
|
||||
} else {
|
||||
warn("[goroutine] #%04d failed: %s", s.Issue.Number, result.Error)
|
||||
}
|
||||
}(i, spec)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
func findSpec(specs []pcore.WorktreeSpec, issueNum int) *pcore.WorktreeSpec {
|
||||
for _, s := range specs {
|
||||
if s.Issue.Number == issueNum {
|
||||
return &s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func info(format string, args ...any) {
|
||||
fmt.Printf("%s[INFO]%s %s\n", blue, nc, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func ok(format string, args ...any) {
|
||||
fmt.Printf("%s[OK]%s %s\n", green, nc, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func warn(format string, args ...any) {
|
||||
fmt.Printf("%s[WARN]%s %s\n", yellow, nc, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "%s[ERROR]%s %s\n", red, nc, fmt.Sprintf(format, args...))
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lucasdataproyects/devfactory/core"
|
||||
dfshell "github.com/lucasdataproyects/devfactory/shell"
|
||||
|
||||
pcore "github.com/lucasdataproyects/parallel-executor/core"
|
||||
)
|
||||
|
||||
const defaultTimeout = 30 * time.Minute
|
||||
|
||||
// ExecuteIssue ejecuta claude para resolver una issue en un worktree.
|
||||
// Invoca `claude -p` con el prompt de fix-issue dentro del worktree.
|
||||
func ExecuteIssue(spec pcore.WorktreeSpec, timeout time.Duration) pcore.ExecutionResult {
|
||||
start := time.Now()
|
||||
|
||||
if timeout == 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(
|
||||
"Read the issue file dev/issues/%04d-*.md and implement all tasks. "+
|
||||
"Run tests after each change. Follow pure core / impure shell pattern. "+
|
||||
"When done, commit all changes with a descriptive message.",
|
||||
spec.Issue.Number,
|
||||
)
|
||||
|
||||
result := dfshell.RunWithTimeout("claude",
|
||||
timeout,
|
||||
"-p", prompt,
|
||||
"--cwd", spec.WorkDir,
|
||||
)
|
||||
|
||||
duration := time.Since(start).Round(time.Second).String()
|
||||
|
||||
if result.IsErr() {
|
||||
return pcore.ExecutionResult{
|
||||
Issue: spec.Issue,
|
||||
Success: false,
|
||||
Duration: duration,
|
||||
Error: result.Error().Error(),
|
||||
}
|
||||
}
|
||||
|
||||
cmdResult := result.Unwrap()
|
||||
if !cmdResult.Success() {
|
||||
return pcore.ExecutionResult{
|
||||
Issue: spec.Issue,
|
||||
Success: false,
|
||||
Duration: duration,
|
||||
Error: cmdResult.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
return pcore.ExecutionResult{
|
||||
Issue: spec.Issue,
|
||||
Success: true,
|
||||
Duration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
// PushWorktreeBranch hace push de la branch del worktree al remote.
|
||||
func PushWorktreeBranch(spec pcore.WorktreeSpec) core.Result[struct{}] {
|
||||
result := dfshell.Run("git", "-C", spec.WorkDir,
|
||||
"push", "-u", "origin", spec.BranchName,
|
||||
)
|
||||
if result.IsErr() {
|
||||
return core.Err[struct{}](fmt.Errorf("push failed for %s: %w",
|
||||
spec.BranchName, result.Error()))
|
||||
}
|
||||
return core.Ok(struct{}{})
|
||||
}
|
||||
|
||||
// MergeBranchToMaster mergea una branch a master con --no-ff.
|
||||
func MergeBranchToMaster(branchName string, repoRoot string) core.Result[struct{}] {
|
||||
// Checkout master
|
||||
result := dfshell.Run("git", "-C", repoRoot, "checkout", "master")
|
||||
if result.IsErr() {
|
||||
return core.Err[struct{}](result.Error())
|
||||
}
|
||||
|
||||
// Merge --no-ff
|
||||
message := fmt.Sprintf("merge: %s — parallel execution", branchName)
|
||||
result = dfshell.Run("git", "-C", repoRoot,
|
||||
"merge", "--no-ff", "-m", message, branchName,
|
||||
)
|
||||
if result.IsErr() {
|
||||
return core.Err[struct{}](fmt.Errorf("merge failed for %s: %w",
|
||||
branchName, result.Error()))
|
||||
}
|
||||
|
||||
return core.Ok(struct{}{})
|
||||
}
|
||||
|
||||
// DeleteBranch elimina una branch local.
|
||||
func DeleteBranch(branchName string, repoRoot string) core.Result[struct{}] {
|
||||
result := dfshell.Run("git", "-C", repoRoot, "branch", "-d", branchName)
|
||||
if result.IsErr() {
|
||||
return core.Err[struct{}](result.Error())
|
||||
}
|
||||
return core.Ok(struct{}{})
|
||||
}
|
||||
|
||||
// ReadIssueFiles lee todos los archivos de issues de un directorio.
|
||||
func ReadIssueFiles(issuesDir string) core.Result[map[string]string] {
|
||||
entries := dfshell.ListDir(issuesDir)
|
||||
if entries.IsErr() {
|
||||
return core.Err[map[string]string](entries.Error())
|
||||
}
|
||||
|
||||
files := make(map[string]string)
|
||||
for _, entry := range entries.Unwrap() {
|
||||
if !strings.HasSuffix(entry, ".md") || entry == "README.md" {
|
||||
continue
|
||||
}
|
||||
content := dfshell.ReadString(issuesDir + "/" + entry)
|
||||
if content.IsOk() {
|
||||
files[entry] = content.Unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
return core.Ok(files)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lucasdataproyects/devfactory/core"
|
||||
dfshell "github.com/lucasdataproyects/devfactory/shell"
|
||||
|
||||
pcore "github.com/lucasdataproyects/parallel-executor/core"
|
||||
)
|
||||
|
||||
// Logger escribe logs de la ejecución paralela a disco.
|
||||
type Logger struct {
|
||||
logsDir string
|
||||
sessionID string
|
||||
}
|
||||
|
||||
// NewLogger crea un logger para la sesión actual.
|
||||
func NewLogger(repoRoot string) core.Result[*Logger] {
|
||||
sessionID := time.Now().Format("20060102-150405")
|
||||
logsDir := repoRoot + "/logs"
|
||||
|
||||
result := dfshell.MkdirAll(logsDir)
|
||||
if result.IsErr() {
|
||||
return core.Err[*Logger](result.Error())
|
||||
}
|
||||
|
||||
return core.Ok(&Logger{
|
||||
logsDir: logsDir,
|
||||
sessionID: sessionID,
|
||||
})
|
||||
}
|
||||
|
||||
// LogIssueStart registra el inicio de ejecución de una issue.
|
||||
func (l *Logger) LogIssueStart(issue pcore.Issue) {
|
||||
msg := fmt.Sprintf("[%s] START Issue #%04d - %s\n",
|
||||
time.Now().Format("15:04:05"), issue.Number, issue.Title)
|
||||
l.appendToSession(msg)
|
||||
}
|
||||
|
||||
// LogIssueResult registra el resultado de una issue.
|
||||
func (l *Logger) LogIssueResult(result pcore.ExecutionResult) {
|
||||
status := "SUCCESS"
|
||||
if !result.Success {
|
||||
status = "FAILED"
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("[%s] %s Issue #%04d - %s (duration: %s)",
|
||||
time.Now().Format("15:04:05"), status,
|
||||
result.Issue.Number, result.Issue.Title, result.Duration)
|
||||
|
||||
if result.Error != "" {
|
||||
msg += "\n Error: " + result.Error
|
||||
}
|
||||
msg += "\n"
|
||||
|
||||
l.appendToSession(msg)
|
||||
|
||||
// Log individual por issue
|
||||
issueLogFile := fmt.Sprintf("%s/issue-%04d-%s.log",
|
||||
l.logsDir, result.Issue.Number, l.sessionID)
|
||||
content := fmt.Sprintf("Issue #%04d - %s\nStatus: %s\nDuration: %s\n",
|
||||
result.Issue.Number, result.Issue.Title, status, result.Duration)
|
||||
if result.Error != "" {
|
||||
content += "Error:\n" + result.Error + "\n"
|
||||
}
|
||||
dfshell.WriteString(issueLogFile, content)
|
||||
}
|
||||
|
||||
// WriteSummary escribe el resumen consolidado.
|
||||
func (l *Logger) WriteSummary(results []pcore.ExecutionResult) core.Result[string] {
|
||||
summaryFile := fmt.Sprintf("%s/summary-%s.txt", l.logsDir, l.sessionID)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("=" + strings.Repeat("=", 59) + "\n")
|
||||
b.WriteString(fmt.Sprintf(" Parallel Execution Summary — %s\n", l.sessionID))
|
||||
b.WriteString("=" + strings.Repeat("=", 59) + "\n\n")
|
||||
|
||||
succeeded := 0
|
||||
failed := 0
|
||||
for _, r := range results {
|
||||
if r.Success {
|
||||
succeeded++
|
||||
} else {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("Total: %d\n", len(results)))
|
||||
b.WriteString(fmt.Sprintf("Succeeded: %d\n", succeeded))
|
||||
b.WriteString(fmt.Sprintf("Failed: %d\n\n", failed))
|
||||
|
||||
b.WriteString("Results:\n")
|
||||
b.WriteString(strings.Repeat("-", 60) + "\n")
|
||||
|
||||
for _, r := range results {
|
||||
status := "✓"
|
||||
if !r.Success {
|
||||
status = "✗"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s #%04d %-30s %s\n",
|
||||
status, r.Issue.Number, r.Issue.Title, r.Duration))
|
||||
if r.Error != "" {
|
||||
b.WriteString(fmt.Sprintf(" Error: %s\n", truncate(r.Error, 80)))
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString(strings.Repeat("-", 60) + "\n")
|
||||
|
||||
writeResult := dfshell.WriteString(summaryFile, b.String())
|
||||
if writeResult.IsErr() {
|
||||
return core.Err[string](writeResult.Error())
|
||||
}
|
||||
|
||||
return core.Ok(summaryFile)
|
||||
}
|
||||
|
||||
// SessionLogFile retorna la ruta del log de sesión.
|
||||
func (l *Logger) SessionLogFile() string {
|
||||
return fmt.Sprintf("%s/parallel-execution-%s.log", l.logsDir, l.sessionID)
|
||||
}
|
||||
|
||||
func (l *Logger) appendToSession(msg string) {
|
||||
logFile := l.SessionLogFile()
|
||||
dfshell.AppendFile(logFile, []byte(msg))
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max-3] + "..."
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Package shell contiene operaciones I/O del parallel executor.
|
||||
// Todas las funciones retornan Result[T] y tienen side effects (git, filesystem).
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lucasdataproyects/devfactory/core"
|
||||
dfshell "github.com/lucasdataproyects/devfactory/shell"
|
||||
|
||||
pcore "github.com/lucasdataproyects/parallel-executor/core"
|
||||
)
|
||||
|
||||
// CreateWorktree crea un git worktree para una issue.
|
||||
// Crea branch desde master y configura el directorio de trabajo.
|
||||
func CreateWorktree(spec pcore.WorktreeSpec, repoRoot string) core.Result[string] {
|
||||
// Verificar que no existe ya
|
||||
if dfshell.DirExists(spec.WorkDir) {
|
||||
return core.Ok(spec.WorkDir)
|
||||
}
|
||||
|
||||
// Crear directorio padre si no existe
|
||||
parentDir := spec.WorkDir[:strings.LastIndex(spec.WorkDir, "/")]
|
||||
mkResult := dfshell.MkdirAll(parentDir)
|
||||
if mkResult.IsErr() {
|
||||
return core.Err[string](mkResult.Error())
|
||||
}
|
||||
|
||||
// Actualizar master primero
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
updateResult := dfshell.RunWithContext(ctx, "git", "-C", repoRoot, "fetch", "origin", "master")
|
||||
if updateResult.IsErr() {
|
||||
// No fatal — puede no tener remote
|
||||
}
|
||||
|
||||
// Crear worktree con nueva branch desde master
|
||||
result := dfshell.Run("git", "-C", repoRoot,
|
||||
"worktree", "add",
|
||||
"-b", spec.BranchName,
|
||||
spec.WorkDir,
|
||||
"master",
|
||||
)
|
||||
if result.IsErr() {
|
||||
// Branch puede existir — intentar sin -b
|
||||
result = dfshell.Run("git", "-C", repoRoot,
|
||||
"worktree", "add",
|
||||
spec.WorkDir,
|
||||
spec.BranchName,
|
||||
)
|
||||
if result.IsErr() {
|
||||
return core.Err[string](fmt.Errorf("failed to create worktree for issue #%04d: %w",
|
||||
spec.Issue.Number, result.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
return core.Ok(spec.WorkDir)
|
||||
}
|
||||
|
||||
// RemoveWorktree elimina un worktree y su branch.
|
||||
func RemoveWorktree(spec pcore.WorktreeSpec, repoRoot string) core.Result[struct{}] {
|
||||
if !dfshell.DirExists(spec.WorkDir) {
|
||||
return core.Ok(struct{}{})
|
||||
}
|
||||
|
||||
// Remover worktree
|
||||
result := dfshell.Run("git", "-C", repoRoot,
|
||||
"worktree", "remove", "--force", spec.WorkDir,
|
||||
)
|
||||
if result.IsErr() {
|
||||
// Fallback: eliminar directorio manualmente
|
||||
os.RemoveAll(spec.WorkDir)
|
||||
// Prune worktrees huérfanos
|
||||
dfshell.Run("git", "-C", repoRoot, "worktree", "prune")
|
||||
}
|
||||
|
||||
return core.Ok(struct{}{})
|
||||
}
|
||||
|
||||
// CleanupAllWorktrees limpia todos los worktrees del directorio worktrees/.
|
||||
func CleanupAllWorktrees(repoRoot string) core.Result[int] {
|
||||
worktreesDir := repoRoot + "/worktrees"
|
||||
if !dfshell.DirExists(worktreesDir) {
|
||||
return core.Ok(0)
|
||||
}
|
||||
|
||||
entries := dfshell.ListDir(worktreesDir)
|
||||
if entries.IsErr() {
|
||||
return core.Ok(0)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, entry := range entries.Unwrap() {
|
||||
fullPath := worktreesDir + "/" + entry
|
||||
dfshell.Run("git", "-C", repoRoot, "worktree", "remove", "--force", fullPath)
|
||||
count++
|
||||
}
|
||||
|
||||
// Prune
|
||||
dfshell.Run("git", "-C", repoRoot, "worktree", "prune")
|
||||
|
||||
// Eliminar directorio vacío
|
||||
os.Remove(worktreesDir)
|
||||
|
||||
return core.Ok(count)
|
||||
}
|
||||
|
||||
// ListWorktrees devuelve los worktrees activos.
|
||||
func ListWorktrees(repoRoot string) core.Result[[]string] {
|
||||
result := dfshell.Run("git", "-C", repoRoot, "worktree", "list", "--porcelain")
|
||||
if result.IsErr() {
|
||||
return core.Err[[]string](result.Error())
|
||||
}
|
||||
|
||||
output := result.Unwrap().Output()
|
||||
lines := strings.Split(output, "\n")
|
||||
|
||||
var paths []string
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "worktree ") {
|
||||
path := strings.TrimPrefix(line, "worktree ")
|
||||
if path != repoRoot {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return core.Ok(paths)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user