merge: quick/dagu-agent-and-skill — agente dagu, skill dagu-auto y utilidades
This commit is contained in:
@@ -0,0 +1,373 @@
|
|||||||
|
---
|
||||||
|
name: dagu
|
||||||
|
description: Agente para gestionar Dagu - instalar, organizar ~/dagu, crear/editar DAGs y automatizar workflows con YAML
|
||||||
|
model: sonnet
|
||||||
|
tools: Read, Write, Bash, Glob, Grep, Edit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agente Dagu
|
||||||
|
|
||||||
|
Eres un experto en Dagu, el motor de workflows local-first basado en DAGs. Tu rol es gestionar la instalación, configuración y creación de automatizaciones en Dagu.
|
||||||
|
|
||||||
|
## Tu entorno
|
||||||
|
|
||||||
|
- **Directorio base**: `~/dagu/`
|
||||||
|
- **DAGs**: `~/dagu/dags/`
|
||||||
|
- **Scripts**: `~/dagu/scripts/`
|
||||||
|
- **Logs**: `~/dagu/logs/`
|
||||||
|
- **Data**: `~/dagu/data/`
|
||||||
|
- **Config**: `~/dagu/dagu-config.yaml`
|
||||||
|
- **Binario**: `~/.local/bin/dagu`
|
||||||
|
- **Web UI**: http://localhost:8090
|
||||||
|
- **Servicio**: `systemctl --user status dagu.service`
|
||||||
|
|
||||||
|
## Capacidades principales
|
||||||
|
|
||||||
|
### Instalación
|
||||||
|
- Detectar si Dagu está instalado (`which dagu && dagu version`)
|
||||||
|
- Instalar en máquinas nuevas (Linux/macOS/Windows)
|
||||||
|
- Configurar como servicio systemd
|
||||||
|
- Crear estructura de directorios
|
||||||
|
|
||||||
|
### Organización de ~/dagu
|
||||||
|
- Mantener estructura limpia de carpetas
|
||||||
|
- Organizar DAGs por categoría en subcarpetas
|
||||||
|
- Gestionar scripts asociados a DAGs
|
||||||
|
- Limpiar logs y data obsoletos
|
||||||
|
|
||||||
|
### Creación de DAGs
|
||||||
|
- Generar workflows YAML completos
|
||||||
|
- Crear DAGs con dependencias (graph mode)
|
||||||
|
- Configurar schedules con cron
|
||||||
|
- Parametrizar workflows
|
||||||
|
- Crear sub-workflows con `call`
|
||||||
|
|
||||||
|
### Gestión
|
||||||
|
- Validar DAGs (`dagu validate`)
|
||||||
|
- Ver estado (`dagu status`)
|
||||||
|
- Ejecutar DAGs (`dagu start`)
|
||||||
|
- Ver historial (`dagu history`)
|
||||||
|
|
||||||
|
## Instalación en máquinas nuevas
|
||||||
|
|
||||||
|
### Detección
|
||||||
|
```bash
|
||||||
|
if command -v dagu &>/dev/null; then
|
||||||
|
echo "Dagu $(dagu version) ya instalado"
|
||||||
|
else
|
||||||
|
echo "Dagu no encontrado, instalando..."
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux/macOS
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/dagu-org/dagu/main/scripts/installer.sh | bash -s -- --install-dir ~/.local/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Como servicio systemd
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cat > ~/.config/systemd/user/dagu.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Dagu Workflow Scheduler
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/dagu start-all --config=%h/dagu/dagu-config.yaml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now dagu.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estructura inicial
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/dagu/{dags,scripts,logs,data}
|
||||||
|
|
||||||
|
cat > ~/dagu/dagu-config.yaml << 'EOF'
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 8090
|
||||||
|
dags: /home/$USER/dagu/dags
|
||||||
|
logDir: /home/$USER/dagu/logs
|
||||||
|
dataDir: /home/$USER/dagu/data
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Referencia YAML de DAGs
|
||||||
|
|
||||||
|
### Estructura mínima
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- command: echo "Hello from Dagu!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estructura completa
|
||||||
|
```yaml
|
||||||
|
# Metadata
|
||||||
|
name: mi-workflow
|
||||||
|
description: Descripción del workflow
|
||||||
|
tags: [etl, produccion]
|
||||||
|
group: MiGrupo
|
||||||
|
|
||||||
|
# Tipo de ejecución
|
||||||
|
type: graph # "chain" (secuencial) o "graph" (dependencias)
|
||||||
|
|
||||||
|
# Programación cron
|
||||||
|
schedule: "0 2 * * *"
|
||||||
|
# Múltiples: schedule: ["0 9 * * MON-FRI", "0 14 * * SAT,SUN"]
|
||||||
|
# Con timezone: schedule: "CRON_TZ=America/Argentina/Buenos_Aires 0 9 * * *"
|
||||||
|
# Start/stop: schedule: { start: "0 8 * * *", stop: "0 18 * * *" }
|
||||||
|
|
||||||
|
skip_if_successful: true # Saltar si la última ejecución fue exitosa
|
||||||
|
|
||||||
|
# Control de ejecución
|
||||||
|
max_active_steps: 5 # Máximo pasos paralelos (graph mode)
|
||||||
|
timeout_sec: 7200 # Timeout del DAG
|
||||||
|
delay_sec: 10 # Delay antes de iniciar
|
||||||
|
|
||||||
|
# Shell
|
||||||
|
shell: ["/bin/bash", "-e", "-u"]
|
||||||
|
|
||||||
|
# Directorio de trabajo
|
||||||
|
working_dir: /tmp
|
||||||
|
|
||||||
|
# Variables de entorno
|
||||||
|
env:
|
||||||
|
- LOG_LEVEL: info
|
||||||
|
- DATE: "`date '+%Y-%m-%d'`" # Sustitución de comandos con backticks
|
||||||
|
- API_KEY: ${SECRET_API_KEY} # Referencia a env var del sistema
|
||||||
|
|
||||||
|
# Dotenv
|
||||||
|
dotenv: .env
|
||||||
|
|
||||||
|
# Parámetros tipados
|
||||||
|
params:
|
||||||
|
- name: ENVIRONMENT
|
||||||
|
type: string
|
||||||
|
default: production
|
||||||
|
enum: [dev, staging, prod]
|
||||||
|
- name: DRY_RUN
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# Secretos
|
||||||
|
secrets:
|
||||||
|
- name: API_TOKEN
|
||||||
|
provider: env
|
||||||
|
key: PROD_API_TOKEN
|
||||||
|
|
||||||
|
# Precondiciones
|
||||||
|
preconditions:
|
||||||
|
- condition: "`date +%u`"
|
||||||
|
expected: "re:[1-5]" # Regex: solo días laborables
|
||||||
|
|
||||||
|
# Retención de historial
|
||||||
|
hist_retention_days: 90
|
||||||
|
|
||||||
|
# Handlers de ciclo de vida
|
||||||
|
handler_on:
|
||||||
|
success:
|
||||||
|
command: echo "Completado exitosamente"
|
||||||
|
failure:
|
||||||
|
command: echo "Falló la ejecución"
|
||||||
|
exit:
|
||||||
|
command: echo "Siempre se ejecuta"
|
||||||
|
|
||||||
|
# Steps
|
||||||
|
steps:
|
||||||
|
- id: paso_1
|
||||||
|
description: Primer paso
|
||||||
|
command: echo "Paso 1"
|
||||||
|
|
||||||
|
- id: paso_2
|
||||||
|
command: echo "Paso 2"
|
||||||
|
depends: [paso_1] # Solo en graph mode
|
||||||
|
env:
|
||||||
|
- EXTRA_VAR: valor
|
||||||
|
output: RESULTADO # Capturar stdout en variable
|
||||||
|
stdout: /tmp/output.log # Redirigir stdout a archivo
|
||||||
|
continue_on:
|
||||||
|
failure: true # Continuar si falla
|
||||||
|
retry_policy:
|
||||||
|
limit: 3
|
||||||
|
interval_sec: 30
|
||||||
|
backoff: true
|
||||||
|
timeout_sec: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tipos de steps especiales
|
||||||
|
|
||||||
|
#### Sub-workflow (call)
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- call: etl/extract
|
||||||
|
params: "SOURCE=s3://bucket/data"
|
||||||
|
output: EXTRACT_RESULT
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HTTP
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- command: POST https://api.example.com/webhook
|
||||||
|
type: http
|
||||||
|
config:
|
||||||
|
headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
body: '{"status": "started"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- id: build
|
||||||
|
container:
|
||||||
|
image: python:3.11
|
||||||
|
volumes:
|
||||||
|
- ./src:/app
|
||||||
|
working_dir: /app
|
||||||
|
command: python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SSH
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- name: deploy
|
||||||
|
type: ssh
|
||||||
|
config:
|
||||||
|
host: prod-server.example.com
|
||||||
|
user: deploy
|
||||||
|
key: ~/.ssh/id_rsa
|
||||||
|
command: cd /var/www && git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JQ (procesamiento JSON)
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- command: '.data[] | .email'
|
||||||
|
type: jq
|
||||||
|
script: ${API_RESPONSE}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Router (condicional)
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- id: router
|
||||||
|
type: router
|
||||||
|
value: ${STATUS}
|
||||||
|
routes:
|
||||||
|
"production": [prod_handler]
|
||||||
|
"staging": [staging_handler]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parallel Iterator
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- call: processor
|
||||||
|
parallel:
|
||||||
|
items: [A, B, C]
|
||||||
|
max_concurrent: 2
|
||||||
|
params: "ITEM=${ITEM}"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Chat/LLM
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- type: chat
|
||||||
|
llm:
|
||||||
|
provider: anthropic
|
||||||
|
model: claude-sonnet-4-20250514
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "Analiza estos datos..."
|
||||||
|
output: ANSWER
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables especiales de runtime
|
||||||
|
|
||||||
|
| Variable | Descripción |
|
||||||
|
|----------|-------------|
|
||||||
|
| `DAG_NAME` | Nombre del DAG |
|
||||||
|
| `DAG_RUN_ID` | ID único de ejecución |
|
||||||
|
| `DAG_RUN_LOG_FILE` | Ruta al log agregado |
|
||||||
|
| `DAG_RUN_STEP_NAME` | Nombre del step actual |
|
||||||
|
| `DAG_RUN_STATUS` | Estado en handlers |
|
||||||
|
| `DAG_PARAMS_JSON` | JSON de parámetros |
|
||||||
|
|
||||||
|
### Paso de datos entre steps
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- command: git rev-parse --short HEAD
|
||||||
|
output: VERSION
|
||||||
|
- command: echo "Versión: ${VERSION}"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON Path
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- command: echo '{"db": {"host": "localhost", "port": 5432}}'
|
||||||
|
output: CONFIG
|
||||||
|
- command: psql -h ${CONFIG.db.host} -p ${CONFIG.db.port}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Referencia por step ID
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- id: extract
|
||||||
|
command: python extract.py
|
||||||
|
output: DATA
|
||||||
|
- command: echo "Exit: ${extract.exit_code}, Output: ${extract.output}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flujo de trabajo
|
||||||
|
|
||||||
|
1. **Verificar instalación**: Comprobar que Dagu está instalado y corriendo
|
||||||
|
2. **Entender la necesidad**: Qué quiere automatizar el usuario
|
||||||
|
3. **Diseñar el DAG**: Elegir chain vs graph, definir steps y dependencias
|
||||||
|
4. **Crear archivos**: DAG YAML + scripts necesarios en ~/dagu/
|
||||||
|
5. **Validar**: `dagu validate ~/dagu/dags/nombre.yaml`
|
||||||
|
6. **Probar**: `dagu start nombre` o test desde Web UI
|
||||||
|
|
||||||
|
## Comandos útiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Estado del servicio
|
||||||
|
systemctl --user status dagu.service
|
||||||
|
systemctl --user restart dagu.service
|
||||||
|
|
||||||
|
# Gestión de DAGs
|
||||||
|
dagu start nombre.yaml # Ejecutar
|
||||||
|
dagu start nombre.yaml -- PARAM=valor # Con parámetros
|
||||||
|
dagu validate nombre.yaml # Validar
|
||||||
|
dagu status nombre # Estado
|
||||||
|
dagu history nombre # Historial
|
||||||
|
|
||||||
|
# Web UI + scheduler
|
||||||
|
dagu start-all --config=~/dagu/dagu-config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convenciones
|
||||||
|
|
||||||
|
- DAGs en `~/dagu/dags/` con extensión `.yaml`
|
||||||
|
- Scripts auxiliares en `~/dagu/scripts/`
|
||||||
|
- Nombres de DAG en snake_case o kebab-case
|
||||||
|
- Siempre incluir `name` y `description` en el DAG
|
||||||
|
- Usar `type: graph` cuando hay dependencias entre steps
|
||||||
|
- Preferir `id` sobre `name` en steps para referenciarlos
|
||||||
|
- Validar siempre antes de activar un schedule
|
||||||
|
- Organizar DAGs complejos en subcarpetas temáticas
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
- Dagu corre como servicio systemd del usuario en esta máquina
|
||||||
|
- El puerto configurado es 8090 (no el default 8080)
|
||||||
|
- La config está en `~/dagu/dagu-config.yaml` (no en ~/.config/dagu/)
|
||||||
|
- Preferimos Dagu sobre cron para TODA programación de tareas
|
||||||
|
- El filtrado de env vars de Dagu solo pasa: PATH, HOME, USER, SHELL, TMPDIR, TERM, LANG, TZ, DAGU_*, LC_*, DAG_*
|
||||||
|
- Para pasar otras env vars, definirlas explícitamente en el DAG
|
||||||
@@ -4,5 +4,8 @@
|
|||||||
"command": "~/.claude/statusline.sh",
|
"command": "~/.claude/statusline.sh",
|
||||||
"padding": 1
|
"padding": 1
|
||||||
},
|
},
|
||||||
|
"enabledPlugins": {
|
||||||
|
"gopls-lsp@claude-plugins-official": true
|
||||||
|
},
|
||||||
"skipDangerousModePermissionPrompt": true
|
"skipDangerousModePermissionPrompt": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
+1481
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
|||||||
|
---
|
||||||
|
name: dagu-auto
|
||||||
|
description: Genera automatizaciones Dagu (DAGs YAML) - crea workflows, schedules y scripts en ~/dagu/. Usar en vez de cron para cualquier tarea programada.
|
||||||
|
argument-hint: [descripción de la automatización]
|
||||||
|
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# dagu-auto
|
||||||
|
|
||||||
|
Genera una automatización completa en Dagu: DAG YAML + scripts necesarios.
|
||||||
|
|
||||||
|
**Preferimos Dagu sobre cron para TODA programación de tareas.**
|
||||||
|
|
||||||
|
## Sintaxis
|
||||||
|
|
||||||
|
```
|
||||||
|
/dagu-auto backup diario de base de datos
|
||||||
|
/dagu-auto ETL pipeline cada hora
|
||||||
|
/dagu-auto limpiar logs viejos cada domingo
|
||||||
|
/dagu-auto monitorear API cada 5 minutos
|
||||||
|
```
|
||||||
|
|
||||||
|
O Claude puede invocar esta skill cuando detecte que el usuario necesita programar/automatizar algo.
|
||||||
|
|
||||||
|
## Precondiciones
|
||||||
|
|
||||||
|
- [ ] Dagu instalado (`which dagu`)
|
||||||
|
- [ ] Directorio `~/dagu/dags/` existe
|
||||||
|
- [ ] Servicio dagu corriendo (`systemctl --user is-active dagu.service`)
|
||||||
|
|
||||||
|
Si no se cumplen, instalar y configurar primero:
|
||||||
|
```bash
|
||||||
|
# Instalar
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/dagu-org/dagu/main/scripts/installer.sh | bash -s -- --install-dir ~/.local/bin
|
||||||
|
|
||||||
|
# Crear estructura
|
||||||
|
mkdir -p ~/dagu/{dags,scripts,logs,data}
|
||||||
|
|
||||||
|
# Configurar servicio
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cat > ~/.config/systemd/user/dagu.service << 'SVCEOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Dagu Workflow Scheduler
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/dagu start-all --config=%h/dagu/dagu-config.yaml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
SVCEOF
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now dagu.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flujo
|
||||||
|
|
||||||
|
### 1. Analizar la solicitud
|
||||||
|
|
||||||
|
Determinar del input ($ARGUMENTS o descripción del usuario):
|
||||||
|
- **Qué automatizar**: la tarea concreta
|
||||||
|
- **Frecuencia**: cron expression (si aplica)
|
||||||
|
- **Dependencias**: pasos secuenciales o paralelos
|
||||||
|
- **Scripts necesarios**: bash, python, go, etc.
|
||||||
|
- **Variables/Parámetros**: configuración dinámica
|
||||||
|
|
||||||
|
### 2. Elegir tipo de DAG
|
||||||
|
|
||||||
|
| Situación | Tipo |
|
||||||
|
|-----------|------|
|
||||||
|
| Pasos secuenciales simples | `type: chain` (default) |
|
||||||
|
| Pasos con dependencias complejas | `type: graph` |
|
||||||
|
| Pasos paralelos | `type: graph` + `max_active_steps` |
|
||||||
|
| Sub-workflows reutilizables | `call:` + archivo separado |
|
||||||
|
|
||||||
|
### 3. Generar nombre del DAG
|
||||||
|
|
||||||
|
```
|
||||||
|
# Convención: snake_case, descriptivo, corto
|
||||||
|
backup_postgres_diario
|
||||||
|
etl_ventas_hora
|
||||||
|
limpieza_logs_semanal
|
||||||
|
monitor_api_health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Crear scripts auxiliares (si necesario)
|
||||||
|
|
||||||
|
Si el step requiere lógica compleja, crear script en `~/dagu/scripts/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ~/dagu/scripts/nombre_script.sh
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
# Lógica aquí
|
||||||
|
```
|
||||||
|
|
||||||
|
Siempre dar permisos de ejecución: `chmod +x ~/dagu/scripts/nombre_script.sh`
|
||||||
|
|
||||||
|
### 5. Generar el DAG YAML
|
||||||
|
|
||||||
|
Crear en `~/dagu/dags/nombre.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: nombre-descriptivo
|
||||||
|
description: Qué hace este workflow
|
||||||
|
tags: [categoria]
|
||||||
|
|
||||||
|
# Schedule (si aplica)
|
||||||
|
schedule: "expresion_cron"
|
||||||
|
|
||||||
|
# Variables de entorno
|
||||||
|
env:
|
||||||
|
- VAR_NECESARIA: valor
|
||||||
|
|
||||||
|
# Parámetros (si necesita configuración)
|
||||||
|
params:
|
||||||
|
- name: PARAM
|
||||||
|
type: string
|
||||||
|
default: valor
|
||||||
|
|
||||||
|
# Handlers
|
||||||
|
handler_on:
|
||||||
|
failure:
|
||||||
|
command: echo "FALLÓ: ${DAG_NAME}" >> ~/dagu/logs/failures.log
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- id: paso_1
|
||||||
|
description: Qué hace este paso
|
||||||
|
command: echo "ejecutando"
|
||||||
|
|
||||||
|
- id: paso_2
|
||||||
|
command: bash ~/dagu/scripts/mi_script.sh
|
||||||
|
depends: [paso_1]
|
||||||
|
retry_policy:
|
||||||
|
limit: 3
|
||||||
|
interval_sec: 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Validar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dagu validate ~/dagu/dags/nombre.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Si falla, corregir y re-validar.
|
||||||
|
|
||||||
|
### 7. Probar ejecución
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dagu start ~/dagu/dags/nombre.yaml
|
||||||
|
# O con parámetros:
|
||||||
|
dagu start ~/dagu/dags/nombre.yaml -- PARAM=valor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Confirmar resultado
|
||||||
|
|
||||||
|
Mostrar al usuario:
|
||||||
|
```
|
||||||
|
DAG creado: ~/dagu/dags/nombre.yaml
|
||||||
|
Schedule: cada día a las 2:00 AM
|
||||||
|
Steps: 3 pasos (graph mode)
|
||||||
|
Scripts: ~/dagu/scripts/nombre_script.sh
|
||||||
|
|
||||||
|
Web UI: http://localhost:8090
|
||||||
|
Validación: OK
|
||||||
|
Test: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
## Referencia rápida de cron
|
||||||
|
|
||||||
|
| Expresión | Significado |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `"*/5 * * * *"` | Cada 5 minutos |
|
||||||
|
| `"0 * * * *"` | Cada hora |
|
||||||
|
| `"0 */2 * * *"` | Cada 2 horas |
|
||||||
|
| `"0 9 * * *"` | Cada día a las 9:00 |
|
||||||
|
| `"0 2 * * *"` | Cada día a las 2:00 AM |
|
||||||
|
| `"0 9 * * MON-FRI"` | Lunes a viernes a las 9:00 |
|
||||||
|
| `"0 0 * * SUN"` | Cada domingo a medianoche |
|
||||||
|
| `"0 0 1 * *"` | Primer día de cada mes |
|
||||||
|
| `"CRON_TZ=America/Argentina/Buenos_Aires 0 9 * * *"` | Con timezone |
|
||||||
|
|
||||||
|
## Referencia rápida de tipos de step
|
||||||
|
|
||||||
|
| Tipo | Uso |
|
||||||
|
|------|-----|
|
||||||
|
| `command:` | Comando shell (default) |
|
||||||
|
| `type: http` | Petición HTTP (GET/POST/PUT/DELETE) |
|
||||||
|
| `type: ssh` | Ejecutar en servidor remoto |
|
||||||
|
| `type: jq` | Procesar JSON |
|
||||||
|
| `type: mail` | Enviar email |
|
||||||
|
| `type: chat` | LLM (OpenAI/Anthropic) |
|
||||||
|
| `type: router` | Condicional/branching |
|
||||||
|
| `type: archive` | Comprimir/descomprimir |
|
||||||
|
| `call:` | Sub-workflow |
|
||||||
|
|
||||||
|
## Convenciones
|
||||||
|
|
||||||
|
- Nombres de DAG en snake_case
|
||||||
|
- Siempre incluir `name`, `description`, `tags`
|
||||||
|
- Un handler_on.failure mínimo para logging
|
||||||
|
- Scripts en `~/dagu/scripts/` con `chmod +x`
|
||||||
|
- Validar siempre antes de activar schedule
|
||||||
|
- Usar `type: graph` cuando hay dependencias
|
||||||
|
- Usar `retry_policy` en steps que pueden fallar (HTTP, SSH)
|
||||||
|
- Usar `output:` para pasar datos entre steps
|
||||||
|
|
||||||
|
## Reglas
|
||||||
|
|
||||||
|
- SIEMPRE verificar que Dagu está instalado antes de crear DAGs
|
||||||
|
- SIEMPRE validar el DAG después de crearlo
|
||||||
|
- NUNCA crear crontabs — usar Dagu schedule en su lugar
|
||||||
|
- NUNCA usar rutas relativas en commands — usar rutas absolutas
|
||||||
|
- NUNCA hardcodear secretos — usar `secrets:` o `env:` con referencias
|
||||||
|
- Si el usuario pide "programar algo" o "ejecutar periódicamente", usar Dagu
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: execute-parallel
|
name: execute-parallel
|
||||||
description: Ejecuta automáticamente issues del plan de ejecución paralela
|
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
|
disable-model-invocation: true
|
||||||
user-invocable: true
|
user-invocable: true
|
||||||
allowed-tools: Bash, Read, Write
|
allowed-tools: Bash, Read, Write
|
||||||
@@ -9,7 +9,7 @@ allowed-tools: Bash, Read, Write
|
|||||||
|
|
||||||
# execute-parallel
|
# 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
|
## Sintaxis
|
||||||
|
|
||||||
@@ -17,75 +17,92 @@ Ejecuta automáticamente las issues del plan paralelo. Crea worktrees, ejecuta /
|
|||||||
/execute-parallel # Ejecutar TODOS los grupos
|
/execute-parallel # Ejecutar TODOS los grupos
|
||||||
/execute-parallel --group 1 # Solo Grupo 1
|
/execute-parallel --group 1 # Solo Grupo 1
|
||||||
/execute-parallel --sequential # Sin paralelismo
|
/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
|
## Flujo
|
||||||
|
|
||||||
### 1. Validar precondiciones
|
### 1. Verificar binario
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Si no existe plan, generarlo automáticamente
|
EXECUTOR="./bin/parallel-executor"
|
||||||
if [ ! -f "PARALLEL_EXECUTION_ORDER.md" ]; then
|
if [ ! -f "$EXECUTOR" ]; then
|
||||||
/parallel-issues
|
echo "Compilando parallel-executor..."
|
||||||
|
cd utils/parallel-executor && make build && cd ../..
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Parsear argumentos
|
### 2. Parsear argumentos del usuario y ejecutar
|
||||||
|
|
||||||
- `--group <N>`: ejecutar solo ese grupo
|
Mapear los argumentos directamente al binario:
|
||||||
- `--sequential`: ejecutar uno a uno
|
|
||||||
- Sin args: ejecutar todos los grupos
|
|
||||||
|
|
||||||
### 3. Ejecutar programa Go
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./cmd/parallel-executor/parallel-executor $ARGS
|
./bin/parallel-executor $ARGS
|
||||||
```
|
```
|
||||||
|
|
||||||
El orquestador Go maneja:
|
**Flags disponibles:**
|
||||||
- Creación de worktrees
|
- `--sort` → analizar issues y generar PARALLEL_EXECUTION_ORDER.md
|
||||||
- Ejecución paralela de `/fix-issue`
|
- `--dry-run` → mostrar plan y worktrees que se crearían
|
||||||
- Push de cada rama
|
- `--group N` → ejecutar solo grupo N
|
||||||
- Limpieza de worktrees
|
- `--sequential` → ejecutar sin paralelismo
|
||||||
- Logging en `logs/`
|
- `--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
|
utils/parallel-executor/
|
||||||
|
├── main.go # CLI + orquestación
|
||||||
Logs:
|
├── core/
|
||||||
- logs/parallel-execution-*.log
|
│ ├── parser.go # Parseo del plan markdown (puro)
|
||||||
- logs/consolidated-summary.txt
|
│ ├── planner.go # Topological sort + conflictos (puro)
|
||||||
|
│ ├── parser_test.go
|
||||||
Worktrees restantes: (ninguno)
|
│ └── planner_test.go
|
||||||
```
|
├── shell/
|
||||||
|
│ ├── worktree.go # Git worktree CRUD
|
||||||
### 5. Eliminar plan
|
│ ├── executor.go # Ejecutar claude en worktree
|
||||||
|
│ └── logger.go # Logging a disco
|
||||||
Si exitoso, eliminar `PARALLEL_EXECUTION_ORDER.md`.
|
├── go.mod, go.work # DevFactory como dependencia
|
||||||
|
└── Makefile
|
||||||
## 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Convenciones
|
## Convenciones
|
||||||
|
|
||||||
- Logs persistentes por ejecución
|
- Usa DevFactory: `Result[T]`, `MapSlice`, `FilterSlice`, `Reduce`
|
||||||
- Timeout 30 min por issue
|
- Patrón pure core / impure shell
|
||||||
- Limpieza automática de worktrees
|
- Logs persistentes en `logs/`
|
||||||
- Plan se elimina al completar
|
- Timeout 30 min por issue (configurable)
|
||||||
|
- Limpieza automática de worktrees al terminar
|
||||||
|
- Plan se auto-genera si no existe (`--sort` implícito)
|
||||||
|
|
||||||
## Reglas
|
## Reglas
|
||||||
|
|
||||||
- SIEMPRE generar plan si no existe
|
- SIEMPRE verificar que el binario existe antes de ejecutar
|
||||||
- Solo advertir si hay cambios (no bloquear)
|
- SIEMPRE mostrar dry-run antes de una ejecución real si el usuario no especificó flags
|
||||||
- SIEMPRE limpiar worktrees al terminar
|
- SIEMPRE limpiar worktrees al terminar
|
||||||
|
- Si no hay plan, generarlo automáticamente
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
name: init-frontend
|
||||||
|
description: Inicializa proyecto frontend (React/Vite) o desktop (Wails) con Frontend_Library
|
||||||
|
disable-model-invocation: true
|
||||||
|
user-invocable: true
|
||||||
|
allowed-tools: Bash, Read, Write, Edit
|
||||||
|
---
|
||||||
|
|
||||||
|
# init-frontend
|
||||||
|
|
||||||
|
Inicializa un proyecto frontend (webapp React/Vite) o desktop (Wails + Go + React). Coherente con Frontend_Library y el stack del frontend-lib/build-wails agents.
|
||||||
|
|
||||||
|
## Sintaxis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/init-frontend [nombre] [--wails] [--path /ruta/destino]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `nombre`: nombre del proyecto (kebab-case). Si no se da, se pregunta.
|
||||||
|
- `--wails`: modo desktop con Wails (Go backend + React frontend). Sin flag = webapp pura.
|
||||||
|
- `--path`: directorio destino. Default: directorio actual.
|
||||||
|
|
||||||
|
## Flujo
|
||||||
|
|
||||||
|
### 1. Ejecutar script de setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash "${CLAUDE_SKILL_DIR}/setup-frontend.sh" [nombre] [--wails] [path]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Si el script reporta STATUS: CONFIGURED
|
||||||
|
|
||||||
|
Informar al usuario que el proyecto ya existe.
|
||||||
|
|
||||||
|
### 3. Si el script reporta STATUS: READY
|
||||||
|
|
||||||
|
Mostrar resumen según modo:
|
||||||
|
|
||||||
|
**Webapp:**
|
||||||
|
- `pnpm dev` para desarrollo
|
||||||
|
- `pnpm build` para producción
|
||||||
|
- Frontend_Library linkeada via pnpm
|
||||||
|
|
||||||
|
**Wails:**
|
||||||
|
- `make dev` para desarrollo con hot reload
|
||||||
|
- `make build` para compilar
|
||||||
|
- Frontend_Library + DevFactory integrados
|
||||||
|
- Bindings Go→TS auto-generados
|
||||||
|
|
||||||
|
### 4. Si el script reporta STATUS: ERROR
|
||||||
|
|
||||||
|
Mostrar el error y sugerir corrección.
|
||||||
|
|
||||||
|
## Convenciones
|
||||||
|
|
||||||
|
- pnpm exclusivamente (no npm ni yarn)
|
||||||
|
- React 19 + TypeScript + Vite + Tailwind CSS 4
|
||||||
|
- @anthropic/frontend-lib via pnpm link
|
||||||
|
- Temas OKLCH con semantic tokens
|
||||||
|
- Phosphor Icons
|
||||||
|
- Vite dedupe obligatorio para react/react-dom
|
||||||
|
- En modo Wails: go.work con DevFactory, patrón pure core / impure shell
|
||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# setup-frontend.sh — Inicializa proyecto React/Vite o Wails desktop
|
||||||
|
# Coherente con Frontend_Library + DevFactory + build-wails agent
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Colores ---
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
|
||||||
|
|
||||||
|
# --- Parámetros ---
|
||||||
|
PROJECT_NAME=""
|
||||||
|
WAILS_MODE=false
|
||||||
|
TARGET_PATH="."
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--wails) WAILS_MODE=true; shift ;;
|
||||||
|
--path) TARGET_PATH="$2"; shift 2 ;;
|
||||||
|
-*) log_error "Flag desconocido: $1"; echo "STATUS: ERROR"; exit 1 ;;
|
||||||
|
*) PROJECT_NAME="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Rutas de librerías ---
|
||||||
|
FRONTEND_LIB="$HOME/.local_agentes/frontend/frontend"
|
||||||
|
DEVFACTORY_PATH="$HOME/.local_agentes/backend"
|
||||||
|
TEMPLATES_DIR="$HOME/.local_agentes/frontend/templates/base"
|
||||||
|
WAILS_TEMPLATES="$HOME/.claude/agents/build-wails/templates"
|
||||||
|
|
||||||
|
# --- Validar nombre ---
|
||||||
|
if [[ -z "$PROJECT_NAME" ]]; then
|
||||||
|
log_error "Uso: setup-frontend.sh <nombre> [--wails] [--path /ruta]"
|
||||||
|
echo "STATUS: ERROR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Normalizar
|
||||||
|
PROJECT_NAME=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
|
||||||
|
PROJECT_DIR="$TARGET_PATH/$PROJECT_NAME"
|
||||||
|
|
||||||
|
# --- Check estado existente ---
|
||||||
|
if [[ -f "$PROJECT_DIR/package.json" ]] || [[ -f "$PROJECT_DIR/wails.json" ]]; then
|
||||||
|
log_warn "El proyecto $PROJECT_NAME ya existe en $PROJECT_DIR"
|
||||||
|
echo "STATUS: CONFIGURED"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Verificar dependencias ---
|
||||||
|
log_step "Verificando dependencias..."
|
||||||
|
|
||||||
|
if ! command -v pnpm &>/dev/null; then
|
||||||
|
log_error "pnpm no está instalado. Instala con: npm install -g pnpm"
|
||||||
|
echo "STATUS: ERROR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_ok "pnpm $(pnpm --version) encontrado"
|
||||||
|
|
||||||
|
if ! command -v node &>/dev/null; then
|
||||||
|
log_error "Node.js no encontrado"
|
||||||
|
echo "STATUS: ERROR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_ok "Node $(node --version) encontrado"
|
||||||
|
|
||||||
|
if [[ "$WAILS_MODE" == true ]]; then
|
||||||
|
if ! command -v wails &>/dev/null; then
|
||||||
|
log_error "Wails no está instalado. Instala con: go install github.com/wailsapp/wails/v2/cmd/wails@latest"
|
||||||
|
echo "STATUS: ERROR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_ok "Wails $(wails version 2>/dev/null | head -1 || echo 'v2.x') encontrado"
|
||||||
|
|
||||||
|
if ! command -v go &>/dev/null; then
|
||||||
|
log_error "Go no encontrado (requerido para Wails)"
|
||||||
|
echo "STATUS: ERROR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_ok "Go $(go version | grep -oP '\d+\.\d+' | head -1) encontrado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$FRONTEND_LIB" ]]; then
|
||||||
|
log_warn "Frontend_Library no encontrada en $FRONTEND_LIB — se creará sin link"
|
||||||
|
HAS_FRONTEND_LIB=false
|
||||||
|
else
|
||||||
|
log_ok "Frontend_Library encontrada"
|
||||||
|
HAS_FRONTEND_LIB=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MODO WAILS — Desktop app (Go + React)
|
||||||
|
# ============================================================================
|
||||||
|
if [[ "$WAILS_MODE" == true ]]; then
|
||||||
|
log_step "Creando proyecto Wails '$PROJECT_NAME'..."
|
||||||
|
|
||||||
|
# Usar wails init con template react-ts
|
||||||
|
wails init -n "$PROJECT_NAME" -t react-ts -d "$TARGET_PATH" 2>/dev/null
|
||||||
|
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# --- go.work con DevFactory ---
|
||||||
|
if [[ -d "$DEVFACTORY_PATH" ]]; then
|
||||||
|
log_step "Configurando go.work con DevFactory..."
|
||||||
|
cat > go.work << EOF
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
use (
|
||||||
|
.
|
||||||
|
$DEVFACTORY_PATH
|
||||||
|
)
|
||||||
|
EOF
|
||||||
|
log_ok "DevFactory enlazado via go.work"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Configurar frontend con pnpm ---
|
||||||
|
log_step "Configurando frontend con pnpm..."
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# Reemplazar npm por pnpm en wails.json
|
||||||
|
cd ..
|
||||||
|
if [[ -f "wails.json" ]]; then
|
||||||
|
sed -i 's/"npm install"/"pnpm install"/g' wails.json
|
||||||
|
sed -i 's/"npm run dev"/"pnpm dev"/g' wails.json
|
||||||
|
sed -i 's/"npm run build"/"pnpm build"/g' wails.json
|
||||||
|
fi
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# Instalar con pnpm
|
||||||
|
rm -f package-lock.json 2>/dev/null || true
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# --- Linkear Frontend_Library ---
|
||||||
|
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||||
|
log_step "Linkeando Frontend_Library..."
|
||||||
|
pnpm add "@anthropic/frontend-lib@link:$FRONTEND_LIB"
|
||||||
|
log_ok "@anthropic/frontend-lib linkeada"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Instalar Tailwind CSS 4 ---
|
||||||
|
log_step "Instalando Tailwind CSS 4..."
|
||||||
|
pnpm add -D tailwindcss @tailwindcss/vite
|
||||||
|
|
||||||
|
# --- Configurar vite.config.ts con dedupe y tailwind ---
|
||||||
|
log_step "Configurando Vite (dedupe + tailwind)..."
|
||||||
|
cat > vite.config.ts << 'VEOF'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, './src'),
|
||||||
|
'@wails': resolve(__dirname, './wailsjs'),
|
||||||
|
},
|
||||||
|
dedupe: ['react', 'react-dom'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
VEOF
|
||||||
|
|
||||||
|
# --- CSS base con Tailwind ---
|
||||||
|
cat > src/style.css << 'CSSEOF'
|
||||||
|
@import "tailwindcss";
|
||||||
|
CSSEOF
|
||||||
|
|
||||||
|
# --- Instalar Phosphor Icons ---
|
||||||
|
pnpm add @phosphor-icons/react
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# --- Makefile (basado en build-wails agent) ---
|
||||||
|
log_step "Generando Makefile..."
|
||||||
|
cat > Makefile << 'MKEOF'
|
||||||
|
.PHONY: dev dev-debug build build-prod build-linux build-windows build-all clean generate doctor
|
||||||
|
|
||||||
|
APP_NAME := $(shell basename $(CURDIR))
|
||||||
|
|
||||||
|
## dev: Desarrollo con hot reload
|
||||||
|
dev:
|
||||||
|
wails dev
|
||||||
|
|
||||||
|
## dev-debug: Desarrollo con DevTools
|
||||||
|
dev-debug:
|
||||||
|
wails dev -devtools
|
||||||
|
|
||||||
|
## build: Build para plataforma actual
|
||||||
|
build:
|
||||||
|
wails build
|
||||||
|
|
||||||
|
## build-prod: Build optimizado para producción
|
||||||
|
build-prod:
|
||||||
|
wails build -clean -trimpath -ldflags="-s -w"
|
||||||
|
|
||||||
|
## build-linux: Build para Linux AMD64
|
||||||
|
build-linux:
|
||||||
|
wails build -platform linux/amd64
|
||||||
|
|
||||||
|
## build-windows: Cross-compile para Windows
|
||||||
|
build-windows:
|
||||||
|
wails build -platform windows/amd64
|
||||||
|
|
||||||
|
## build-all: Linux + Windows
|
||||||
|
build-all: build-linux build-windows
|
||||||
|
|
||||||
|
## generate: Regenerar bindings TypeScript
|
||||||
|
generate:
|
||||||
|
wails generate module
|
||||||
|
|
||||||
|
## clean: Limpiar artefactos
|
||||||
|
clean:
|
||||||
|
rm -rf build/bin frontend/dist
|
||||||
|
|
||||||
|
## doctor: Verificar instalación
|
||||||
|
doctor:
|
||||||
|
wails doctor
|
||||||
|
|
||||||
|
## help: Muestra esta ayuda
|
||||||
|
help:
|
||||||
|
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'
|
||||||
|
MKEOF
|
||||||
|
|
||||||
|
# --- .gitignore ---
|
||||||
|
cat >> .gitignore << 'EOF'
|
||||||
|
node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
build/bin/
|
||||||
|
*.exe
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Resumen Wails ---
|
||||||
|
echo ""
|
||||||
|
log_ok "Proyecto Wails '$PROJECT_NAME' creado en $PROJECT_DIR"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Estructura:${NC}"
|
||||||
|
echo " $PROJECT_NAME/"
|
||||||
|
echo " ├── main.go, app.go — Backend Go"
|
||||||
|
echo " ├── go.work — Enlace a DevFactory"
|
||||||
|
echo " ├── frontend/ — React + TypeScript + Vite"
|
||||||
|
echo " │ ├── src/ — Componentes React"
|
||||||
|
echo " │ └── wailsjs/ — Bindings auto-generados"
|
||||||
|
echo " ├── Makefile — dev, build, build-all"
|
||||||
|
echo " └── wails.json — Configuración Wails"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Comandos:${NC}"
|
||||||
|
echo " make dev — Desarrollo con hot reload"
|
||||||
|
echo " make build — Compilar app desktop"
|
||||||
|
echo " make build-all — Linux + Windows"
|
||||||
|
echo " make generate — Regenerar bindings TS"
|
||||||
|
echo ""
|
||||||
|
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||||
|
echo -e "${CYAN}Frontend_Library:${NC}"
|
||||||
|
echo " import { Button, Card } from '@anthropic/frontend-lib'"
|
||||||
|
echo " import { useTheme } from '@anthropic/frontend-lib/hooks'"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
echo "STATUS: READY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MODO WEBAPP — React + Vite (sin Wails)
|
||||||
|
# ============================================================================
|
||||||
|
log_step "Creando proyecto webapp '$PROJECT_NAME'..."
|
||||||
|
|
||||||
|
mkdir -p "$PROJECT_DIR"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# --- Usar template de Frontend_Library si existe ---
|
||||||
|
if [[ -d "$TEMPLATES_DIR" ]]; then
|
||||||
|
log_step "Usando template de Frontend_Library..."
|
||||||
|
cp -r "$TEMPLATES_DIR"/* .
|
||||||
|
cp -r "$TEMPLATES_DIR"/.[!.]* . 2>/dev/null || true
|
||||||
|
else
|
||||||
|
log_step "Creando desde cero con Vite..."
|
||||||
|
|
||||||
|
# package.json
|
||||||
|
cat > package.json << PKGEOF
|
||||||
|
{
|
||||||
|
"name": "$PROJECT_NAME",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PKGEOF
|
||||||
|
|
||||||
|
# Instalar dependencias base
|
||||||
|
pnpm add react react-dom
|
||||||
|
pnpm add -D typescript @types/react @types/react-dom
|
||||||
|
pnpm add -D vite @vitejs/plugin-react
|
||||||
|
pnpm add -D tailwindcss @tailwindcss/vite
|
||||||
|
|
||||||
|
# tsconfig.json
|
||||||
|
cat > tsconfig.json << 'TSEOF'
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"paths": { "@/*": ["./src/*"] },
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
TSEOF
|
||||||
|
|
||||||
|
# vite.config.ts
|
||||||
|
cat > vite.config.ts << 'VEOF'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: { '@': resolve(__dirname, './src') },
|
||||||
|
dedupe: ['react', 'react-dom'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
VEOF
|
||||||
|
|
||||||
|
# index.html
|
||||||
|
cat > index.html << HTMLEOF
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>$PROJECT_NAME</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTMLEOF
|
||||||
|
|
||||||
|
# src/
|
||||||
|
mkdir -p src
|
||||||
|
|
||||||
|
cat > src/main.tsx << 'TSXEOF'
|
||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './app.css'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
TSXEOF
|
||||||
|
|
||||||
|
cat > src/App.tsx << 'TSXEOF'
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-surface text-foreground flex items-center justify-center">
|
||||||
|
<h1 className="text-3xl font-bold">Ready</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
|
TSXEOF
|
||||||
|
|
||||||
|
cat > src/app.css << 'CSSEOF'
|
||||||
|
@import "tailwindcss";
|
||||||
|
CSSEOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Linkear Frontend_Library ---
|
||||||
|
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||||
|
log_step "Linkeando Frontend_Library..."
|
||||||
|
pnpm add "@anthropic/frontend-lib@link:$FRONTEND_LIB"
|
||||||
|
log_ok "@anthropic/frontend-lib linkeada"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Phosphor Icons ---
|
||||||
|
pnpm add @phosphor-icons/react
|
||||||
|
|
||||||
|
# --- .gitignore ---
|
||||||
|
cat > .gitignore << 'EOF'
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
*.local
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Resumen Webapp ---
|
||||||
|
echo ""
|
||||||
|
log_ok "Proyecto webapp '$PROJECT_NAME' creado en $PROJECT_DIR"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Estructura:${NC}"
|
||||||
|
echo " $PROJECT_NAME/"
|
||||||
|
echo " ├── src/"
|
||||||
|
echo " │ ├── App.tsx — Componente principal"
|
||||||
|
echo " │ ├── main.tsx — Entry point"
|
||||||
|
echo " │ └── app.css — Tailwind CSS"
|
||||||
|
echo " ├── vite.config.ts — Vite + Tailwind + dedupe"
|
||||||
|
echo " ├── tsconfig.json — TypeScript strict"
|
||||||
|
echo " └── package.json — pnpm"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Comandos:${NC}"
|
||||||
|
echo " pnpm dev — Servidor de desarrollo"
|
||||||
|
echo " pnpm build — Build de producción"
|
||||||
|
echo " pnpm preview — Preview del build"
|
||||||
|
echo ""
|
||||||
|
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||||
|
echo -e "${CYAN}Frontend_Library:${NC}"
|
||||||
|
echo " import { Button, Card } from '@anthropic/frontend-lib'"
|
||||||
|
echo " import { useTheme } from '@anthropic/frontend-lib/hooks'"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
echo "STATUS: READY"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: init-go-module
|
||||||
|
description: Inicializa un módulo Go funcional con bindings Python (CGO c-shared + ctypes)
|
||||||
|
disable-model-invocation: true
|
||||||
|
user-invocable: true
|
||||||
|
allowed-tools: Bash, Read, Write, Edit
|
||||||
|
---
|
||||||
|
|
||||||
|
# init-go-module
|
||||||
|
|
||||||
|
Inicializa un módulo Go con arquitectura funcional (pure core / impure shell) y bindings Python automáticos via CGO c-shared + ctypes. Coherente con DevFactory y el stack del backend-lib agent.
|
||||||
|
|
||||||
|
## Sintaxis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/init-go-module [nombre] [--path /ruta/destino]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `nombre`: nombre del módulo (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-go-module.sh" [nombre] [path]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Si el script reporta STATUS: CONFIGURED
|
||||||
|
|
||||||
|
Informar al usuario que el módulo ya está configurado.
|
||||||
|
|
||||||
|
### 3. Si el script reporta STATUS: READY
|
||||||
|
|
||||||
|
Mostrar resumen:
|
||||||
|
- Estructura creada
|
||||||
|
- Cómo compilar: `make build`
|
||||||
|
- Cómo generar bindings Python: `make python`
|
||||||
|
- Cómo testear: `make test`
|
||||||
|
- Cómo usar desde Python: `from bindings.modulo import *`
|
||||||
|
|
||||||
|
### 4. Si el script reporta STATUS: ERROR
|
||||||
|
|
||||||
|
Mostrar el error y sugerir corrección.
|
||||||
|
|
||||||
|
## Convenciones
|
||||||
|
|
||||||
|
- Usa DevFactory como dependencia via `go.work` (igual que build-wails)
|
||||||
|
- Patrón pure core / impure shell de DevFactory
|
||||||
|
- `Result[T]` y `Option[T]` del core de DevFactory
|
||||||
|
- Funciones exportadas a Python son thin wrappers en `export/`
|
||||||
|
- El wrapper Python se auto-genera desde los `//export` comments
|
||||||
+466
@@ -0,0 +1,466 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# setup-go-module.sh — Inicializa módulo Go funcional con bindings Python
|
||||||
|
# Coherente con DevFactory (pure core / impure shell) + CGO c-shared + ctypes
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Colores ---
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
|
||||||
|
|
||||||
|
# --- Parámetros ---
|
||||||
|
MODULE_NAME="${1:-}"
|
||||||
|
TARGET_PATH="${2:-.}"
|
||||||
|
DEVFACTORY_PATH="$HOME/.local_agentes/backend"
|
||||||
|
DEVFACTORY_MODULE="github.com/lucasdataproyects/devfactory"
|
||||||
|
|
||||||
|
# --- Validar nombre ---
|
||||||
|
if [[ -z "$MODULE_NAME" ]]; then
|
||||||
|
log_error "Uso: setup-go-module.sh <nombre> [path]"
|
||||||
|
echo "STATUS: ERROR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Normalizar nombre a kebab-case
|
||||||
|
MODULE_NAME=$(echo "$MODULE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
|
||||||
|
PROJECT_DIR="$TARGET_PATH/$MODULE_NAME"
|
||||||
|
|
||||||
|
# --- Check estado existente ---
|
||||||
|
if [[ -f "$PROJECT_DIR/go.mod" ]]; then
|
||||||
|
log_warn "El módulo $MODULE_NAME ya existe en $PROJECT_DIR"
|
||||||
|
if [[ -f "$PROJECT_DIR/export/exports.go" ]]; then
|
||||||
|
log_ok "Bindings Python ya configurados"
|
||||||
|
else
|
||||||
|
log_warn "Falta directorio export/ — ejecuta de nuevo para completar"
|
||||||
|
fi
|
||||||
|
echo "STATUS: CONFIGURED"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Verificar dependencias ---
|
||||||
|
log_step "Verificando dependencias..."
|
||||||
|
|
||||||
|
if ! command -v go &>/dev/null; then
|
||||||
|
log_error "Go no está instalado. Instala Go 1.22+"
|
||||||
|
echo "STATUS: ERROR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
GO_VERSION=$(go version | grep -oP '\d+\.\d+' | head -1)
|
||||||
|
log_ok "Go $GO_VERSION encontrado"
|
||||||
|
|
||||||
|
if ! command -v python3 &>/dev/null; then
|
||||||
|
log_warn "Python3 no encontrado — los bindings se generarán pero no se podrán testear"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$DEVFACTORY_PATH" ]]; then
|
||||||
|
log_warn "DevFactory no encontrado en $DEVFACTORY_PATH — se creará go.mod sin go.work"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Crear estructura ---
|
||||||
|
log_step "Creando estructura del módulo '$MODULE_NAME'..."
|
||||||
|
|
||||||
|
mkdir -p "$PROJECT_DIR"/{core,shell,export,python/bindings,cmd,internal}
|
||||||
|
|
||||||
|
# --- go.mod ---
|
||||||
|
log_step "Generando go.mod..."
|
||||||
|
cat > "$PROJECT_DIR/go.mod" << EOF
|
||||||
|
module github.com/lucasdataproyects/$MODULE_NAME
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require $DEVFACTORY_MODULE v0.0.0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- go.work (si DevFactory existe localmente) ---
|
||||||
|
if [[ -d "$DEVFACTORY_PATH" ]]; then
|
||||||
|
log_step "Generando go.work con DevFactory local..."
|
||||||
|
cat > "$PROJECT_DIR/go.work" << EOF
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
use (
|
||||||
|
.
|
||||||
|
$DEVFACTORY_PATH
|
||||||
|
)
|
||||||
|
EOF
|
||||||
|
log_ok "go.work enlazado a DevFactory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- core/transform.go — Funciones puras de ejemplo ---
|
||||||
|
log_step "Generando core/ (funciones puras)..."
|
||||||
|
cat > "$PROJECT_DIR/core/transform.go" << 'GOEOF'
|
||||||
|
// Package core contiene funciones puras sin side effects.
|
||||||
|
// Todas las funciones son deterministas y composables.
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
df "github.com/lucasdataproyects/devfactory/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToUpper transforma texto a mayúsculas (función pura).
|
||||||
|
func ToUpper(s string) string {
|
||||||
|
return strings.ToUpper(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessItems aplica una transformación a cada elemento usando MapSlice de DevFactory.
|
||||||
|
func ProcessItems(items []string, transform func(string) string) []string {
|
||||||
|
return df.MapSlice(items, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterNonEmpty filtra elementos vacíos usando FilterSlice de DevFactory.
|
||||||
|
func FilterNonEmpty(items []string) []string {
|
||||||
|
return df.FilterSlice(items, func(s string) bool {
|
||||||
|
return len(strings.TrimSpace(s)) > 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeDivide retorna Result[float64] para evitar panic en división por cero.
|
||||||
|
func SafeDivide(a, b float64) df.Result[float64] {
|
||||||
|
if b == 0 {
|
||||||
|
return df.Err[float64](errors.New("division by zero"))
|
||||||
|
}
|
||||||
|
return df.Ok(a / b)
|
||||||
|
}
|
||||||
|
GOEOF
|
||||||
|
|
||||||
|
# --- core/types.go — Tipos exportables a Python ---
|
||||||
|
cat > "$PROJECT_DIR/core/types.go" << 'GOEOF'
|
||||||
|
package core
|
||||||
|
|
||||||
|
// DataPoint representa un punto de datos exportable a Python.
|
||||||
|
// Los campos usan tipos C-compatible para facilitar el binding.
|
||||||
|
type DataPoint struct {
|
||||||
|
Label string
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary es el resultado de un procesamiento, exportable a Python.
|
||||||
|
type Summary struct {
|
||||||
|
Count int
|
||||||
|
Total float64
|
||||||
|
Items []string
|
||||||
|
}
|
||||||
|
GOEOF
|
||||||
|
|
||||||
|
# --- shell/io.go — Operaciones I/O con Result[T] ---
|
||||||
|
log_step "Generando shell/ (operaciones I/O)..."
|
||||||
|
cat > "$PROJECT_DIR/shell/io.go" << 'GOEOF'
|
||||||
|
// Package shell contiene operaciones con side effects, wrapeadas en Result[T].
|
||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
df "github.com/lucasdataproyects/devfactory/core"
|
||||||
|
"github.com/lucasdataproyects/devfactory/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadDataFile lee un archivo y retorna su contenido como Result.
|
||||||
|
func ReadDataFile(path string) df.Result[string] {
|
||||||
|
return shell.ReadString(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteResult escribe un resultado a archivo.
|
||||||
|
func WriteResult(path string, content string) df.Result[struct{}] {
|
||||||
|
return shell.WriteString(path, content)
|
||||||
|
}
|
||||||
|
GOEOF
|
||||||
|
|
||||||
|
# --- export/exports.go — Funciones exportadas via CGO ---
|
||||||
|
log_step "Generando export/ (bindings CGO)..."
|
||||||
|
cat > "$PROJECT_DIR/export/exports.go" << GOEOF
|
||||||
|
// Package main exporta funciones Go como C shared library.
|
||||||
|
// Cada función con //export se expone como símbolo C callable desde Python.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/lucasdataproyects/$MODULE_NAME/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
//export GoToUpper
|
||||||
|
func GoToUpper(input *C.char) *C.char {
|
||||||
|
result := core.ToUpper(C.GoString(input))
|
||||||
|
return C.CString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export GoProcessItems
|
||||||
|
func GoProcessItems(jsonInput *C.char) *C.char {
|
||||||
|
var items []string
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(jsonInput)), &items); err != nil {
|
||||||
|
return C.CString("[]")
|
||||||
|
}
|
||||||
|
result := core.ProcessItems(items, core.ToUpper)
|
||||||
|
out, _ := json.Marshal(result)
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export GoFilterNonEmpty
|
||||||
|
func GoFilterNonEmpty(jsonInput *C.char) *C.char {
|
||||||
|
var items []string
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(jsonInput)), &items); err != nil {
|
||||||
|
return C.CString("[]")
|
||||||
|
}
|
||||||
|
result := core.FilterNonEmpty(items)
|
||||||
|
out, _ := json.Marshal(result)
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export GoSafeDivide
|
||||||
|
func GoSafeDivide(a, b C.double) *C.char {
|
||||||
|
result := core.SafeDivide(float64(a), float64(b))
|
||||||
|
if result.IsErr() {
|
||||||
|
return C.CString(`{"error":"` + result.Error().Error() + `"}`)
|
||||||
|
}
|
||||||
|
out, _ := json.Marshal(map[string]float64{"value": result.Unwrap()})
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export GoFree
|
||||||
|
func GoFree(ptr *C.char) {
|
||||||
|
C.free(unsafe.Pointer(ptr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
GOEOF
|
||||||
|
|
||||||
|
# --- python/bindings/__init__.py — Wrapper ctypes auto-generado ---
|
||||||
|
log_step "Generando python/bindings/ (ctypes wrapper)..."
|
||||||
|
|
||||||
|
# Nombre de la shared library según OS
|
||||||
|
SO_NAME="lib${MODULE_NAME}.so"
|
||||||
|
|
||||||
|
cat > "$PROJECT_DIR/python/bindings/__init__.py" << PYEOF
|
||||||
|
"""
|
||||||
|
Auto-generated Python bindings for $MODULE_NAME.
|
||||||
|
Uses ctypes to call Go functions compiled as C shared library.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from bindings import to_upper, process_items, filter_non_empty, safe_divide
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Localizar la shared library
|
||||||
|
_LIB_DIR = Path(__file__).parent.parent.parent / "build"
|
||||||
|
_LIB_NAME = "$SO_NAME"
|
||||||
|
_LIB_PATH = _LIB_DIR / _LIB_NAME
|
||||||
|
|
||||||
|
if not _LIB_PATH.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Shared library not found at {_LIB_PATH}. "
|
||||||
|
f"Run 'make build' in the project root first."
|
||||||
|
)
|
||||||
|
|
||||||
|
_lib = ctypes.CDLL(str(_LIB_PATH))
|
||||||
|
|
||||||
|
# --- Configurar tipos de retorno ---
|
||||||
|
_lib.GoToUpper.argtypes = [ctypes.c_char_p]
|
||||||
|
_lib.GoToUpper.restype = ctypes.c_char_p
|
||||||
|
|
||||||
|
_lib.GoProcessItems.argtypes = [ctypes.c_char_p]
|
||||||
|
_lib.GoProcessItems.restype = ctypes.c_char_p
|
||||||
|
|
||||||
|
_lib.GoFilterNonEmpty.argtypes = [ctypes.c_char_p]
|
||||||
|
_lib.GoFilterNonEmpty.restype = ctypes.c_char_p
|
||||||
|
|
||||||
|
_lib.GoSafeDivide.argtypes = [ctypes.c_double, ctypes.c_double]
|
||||||
|
_lib.GoSafeDivide.restype = ctypes.c_char_p
|
||||||
|
|
||||||
|
_lib.GoFree.argtypes = [ctypes.c_char_p]
|
||||||
|
_lib.GoFree.restype = None
|
||||||
|
|
||||||
|
|
||||||
|
def to_upper(text: str) -> str:
|
||||||
|
"""Convert text to uppercase using Go core."""
|
||||||
|
result = _lib.GoToUpper(text.encode("utf-8"))
|
||||||
|
return result.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def process_items(items: list[str]) -> list[str]:
|
||||||
|
"""Process items through Go pipeline (ToUpper transformation)."""
|
||||||
|
input_json = json.dumps(items).encode("utf-8")
|
||||||
|
result = _lib.GoProcessItems(input_json)
|
||||||
|
return json.loads(result.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_non_empty(items: list[str]) -> list[str]:
|
||||||
|
"""Filter empty strings using Go core."""
|
||||||
|
input_json = json.dumps(items).encode("utf-8")
|
||||||
|
result = _lib.GoFilterNonEmpty(input_json)
|
||||||
|
return json.loads(result.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_divide(a: float, b: float) -> float:
|
||||||
|
"""Safe division using Go Result type. Raises ValueError on division by zero."""
|
||||||
|
result = _lib.GoSafeDivide(ctypes.c_double(a), ctypes.c_double(b))
|
||||||
|
data = json.loads(result.decode("utf-8"))
|
||||||
|
if "error" in data:
|
||||||
|
raise ValueError(data["error"])
|
||||||
|
return data["value"]
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# --- python/example.py ---
|
||||||
|
cat > "$PROJECT_DIR/python/example.py" << PYEOF
|
||||||
|
"""Example usage of $MODULE_NAME Go bindings from Python."""
|
||||||
|
from bindings import to_upper, process_items, filter_non_empty, safe_divide
|
||||||
|
|
||||||
|
# String transformation
|
||||||
|
print(to_upper("hello from go")) # HELLO FROM GO
|
||||||
|
|
||||||
|
# Batch processing via Go's MapSlice
|
||||||
|
items = ["hello", "world", "from", "go"]
|
||||||
|
print(process_items(items)) # ["HELLO", "WORLD", "FROM", "GO"]
|
||||||
|
|
||||||
|
# Filtering via Go's FilterSlice
|
||||||
|
mixed = ["hello", "", "world", " ", "go"]
|
||||||
|
print(filter_non_empty(mixed)) # ["hello", "world", "go"]
|
||||||
|
|
||||||
|
# Safe division with Result[T] error handling
|
||||||
|
print(safe_divide(10.0, 3.0)) # 3.333...
|
||||||
|
try:
|
||||||
|
safe_divide(10.0, 0.0)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Caught: {e}") # Caught: division by zero
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# --- core/transform_test.go ---
|
||||||
|
log_step "Generando tests..."
|
||||||
|
cat > "$PROJECT_DIR/core/transform_test.go" << 'GOEOF'
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToUpper(t *testing.T) {
|
||||||
|
if got := ToUpper("hello"); got != "HELLO" {
|
||||||
|
t.Errorf("ToUpper(\"hello\") = %q, want %q", got, "HELLO")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterNonEmpty(t *testing.T) {
|
||||||
|
items := []string{"hello", "", "world", " ", "go"}
|
||||||
|
result := FilterNonEmpty(items)
|
||||||
|
if len(result) != 3 {
|
||||||
|
t.Errorf("FilterNonEmpty got %d items, want 3", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeDivide(t *testing.T) {
|
||||||
|
ok := SafeDivide(10, 2)
|
||||||
|
if ok.IsErr() {
|
||||||
|
t.Error("SafeDivide(10, 2) should not error")
|
||||||
|
}
|
||||||
|
if ok.Unwrap() != 5.0 {
|
||||||
|
t.Errorf("SafeDivide(10, 2) = %f, want 5.0", ok.Unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SafeDivide(10, 0)
|
||||||
|
if !err.IsErr() {
|
||||||
|
t.Error("SafeDivide(10, 0) should error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GOEOF
|
||||||
|
|
||||||
|
# --- Makefile ---
|
||||||
|
log_step "Generando Makefile..."
|
||||||
|
cat > "$PROJECT_DIR/Makefile" << MKEOF
|
||||||
|
.PHONY: build test clean python dev
|
||||||
|
|
||||||
|
MODULE_NAME := $MODULE_NAME
|
||||||
|
BUILD_DIR := build
|
||||||
|
SO_NAME := $SO_NAME
|
||||||
|
|
||||||
|
## build: Compila la shared library (.so) para Python
|
||||||
|
build:
|
||||||
|
@mkdir -p \$(BUILD_DIR)
|
||||||
|
cd export && CGO_ENABLED=1 go build -buildmode=c-shared -o ../\$(BUILD_DIR)/\$(SO_NAME) .
|
||||||
|
@echo "✓ Built \$(BUILD_DIR)/\$(SO_NAME)"
|
||||||
|
|
||||||
|
## test: Ejecuta tests de Go
|
||||||
|
test:
|
||||||
|
go test ./core/... ./shell/... -v
|
||||||
|
|
||||||
|
## python: Compila y ejecuta ejemplo Python
|
||||||
|
python: build
|
||||||
|
cd python && python3 example.py
|
||||||
|
|
||||||
|
## clean: Limpia artefactos
|
||||||
|
clean:
|
||||||
|
rm -rf \$(BUILD_DIR)
|
||||||
|
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
## dev: Tests + build en un paso
|
||||||
|
dev: test build
|
||||||
|
@echo "✓ Ready — run 'make python' to test bindings"
|
||||||
|
|
||||||
|
## tidy: go mod tidy
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
## help: Muestra esta ayuda
|
||||||
|
help:
|
||||||
|
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'
|
||||||
|
MKEOF
|
||||||
|
|
||||||
|
# --- .gitignore ---
|
||||||
|
cat > "$PROJECT_DIR/.gitignore" << 'EOF'
|
||||||
|
build/
|
||||||
|
*.so
|
||||||
|
*.h
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- go mod tidy ---
|
||||||
|
log_step "Ejecutando go mod tidy..."
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
if [[ -f "go.work" ]]; then
|
||||||
|
go mod tidy 2>/dev/null || log_warn "go mod tidy falló — revisa el go.work"
|
||||||
|
else
|
||||||
|
go mod tidy 2>/dev/null || log_warn "go mod tidy falló — DevFactory no está disponible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Resumen ---
|
||||||
|
echo ""
|
||||||
|
log_ok "Módulo '$MODULE_NAME' creado en $PROJECT_DIR"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Estructura:${NC}"
|
||||||
|
echo " $MODULE_NAME/"
|
||||||
|
echo " ├── core/ — Funciones puras (sin side effects)"
|
||||||
|
echo " ├── shell/ — Operaciones I/O con Result[T]"
|
||||||
|
echo " ├── export/ — Funciones exportadas via CGO"
|
||||||
|
echo " ├── python/bindings — Wrapper ctypes auto-generado"
|
||||||
|
echo " ├── Makefile — build, test, python, clean"
|
||||||
|
echo " └── go.work — Enlace a DevFactory"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}Comandos:${NC}"
|
||||||
|
echo " make test — Ejecutar tests Go"
|
||||||
|
echo " make build — Compilar shared library"
|
||||||
|
echo " make python — Testear bindings Python"
|
||||||
|
echo " make dev — Test + build en un paso"
|
||||||
|
echo ""
|
||||||
|
echo "STATUS: READY"
|
||||||
@@ -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 |
|
| 008 | [frontend-lib](008-improve-frontend-lib.md) - Versionado, testing | Media | Pendiente |
|
||||||
| 009 | [gitea](009-improve-gitea.md) - Actions, templates | 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
|
## Agentes Completados
|
||||||
|
|
||||||
| Agente | Descripción | Fecha |
|
| Agente | Descripción | Fecha |
|
||||||
|
|||||||
@@ -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