regista 0.2.0

🎬 AI agent director — state-machine-driven pipeline for pi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# AGENTS.md — regista

Guía para agentes de codificación que trabajen en este proyecto.  
Incluye arquitectura, convenciones, comandos, y decisiones de diseño.

---

## 📌 ¿Qué es esto?

`regista` es un **orquestador genérico de agentes** para [`pi`](https://github.com/mariozechner/pi-coding-agent).  
Automatiza un pipeline de desarrollo de software con 4 roles (PO, QA, Dev, Reviewer)  
gobernado por una **máquina de estados** formal con detección de deadlocks,
checkpoint/resume, y salida JSON para CI/CD.

**Filosofía clave**: regista **no sabe nada del proyecto** que orquesta.  
No importa si el proyecto usa Rust, Python, o cualquier cosa. Solo necesita:
1. Dónde están las historias de usuario (archivos `.md`)
2. Qué skills de `pi` usar para cada rol
3. La máquina de estados fija

---

## 🧱 Stack técnico

| Componente | Tecnología |
|------------|------------|
| Lenguaje | **Rust** (edition 2021) |
| CLI | **clap** 4 (derive) |
| Configuración | **TOML** (`serde` + `toml` 0.8) |
| JSON output | **serde_json** 1 |
| Logging | **tracing** + `tracing-subscriber` (env-filter) |
| Error handling | **anyhow** |
| Regex | **regex** (con `LazyLock`) |
| Fechas | **chrono** |
| Glob | **glob** 0.3 |
| Tests | `#[cfg(test)]` + `tempfile` (dev-dependency) |
| Build | `cargo` |

---

## 📁 Estructura del proyecto

```
regista/
├── AGENTS.md                  ← este archivo
├── README.md                  ← descripción general para usuarios
├── DESIGN.md                  ← diseño completo (máquina de estados, arquitectura)
├── HANDOFF.md                 ← handoff de la última sesión (lo implementado y pendiente)
├── Cargo.toml                 ← dependencias y metadata del crate
├── .gitignore
├── src/
│   ├── main.rs                ← CLI (clap), subcomandos, JSON output, exit codes
│   ├── config.rs              ← Config, ProjectConfig, AgentsConfig, LimitsConfig, carga TOML
│   ├── state.rs               ← Status, Actor, Transition, can_transition_to(), ALL
│   ├── story.rs               ← Story, parseo de .md, set_status(), advance_status_in_memory()
│   ├── dependency_graph.rs    ← DependencyGraph, DFS para ciclos, blocks_count()
│   ├── deadlock.rs            ← analyze(), DeadlockResolution, priorización
│   ├── agent.rs               ← invoke_with_retry(), AgentOptions, feedback rico
│   ├── prompts.rs             ← PromptContext, 7 funciones de prompt
│   ├── orchestrator.rs        ← run(), run_real(), run_dry(), process_story(), checkpoint save
│   ├── checkpoint.rs          ← OrchestratorState: save/load/remove (.regista.state.toml)
│   ├── validator.rs           ← validate(): chequeo pre-vuelo de proyecto
│   ├── init.rs                ← init(): scaffolding de proyecto nuevo
│   ├── groom.rs               ← run(): generación de backlog desde spec con bucle validate
│   ├── hooks.rs               ← run_hook(), ejecución de comandos shell post-fase
│   ├── git.rs                 ← snapshot(), rollback(), init_git()
│   └── daemon.rs              ← detach(), status(), kill(), follow(), DaemonState
├── roadmap/                   ← Documentos de diseño de features futuras
│   ├── ROADMAP.md             ← Índice con estado de cada feature
│   ├── 01-paralelismo.md
│   ├── 02-salida-json-ci-cd.md        ← ✅ IMPLEMENTADO
│   ├── 03-dry-run.md                  ← ✅ IMPLEMENTADO
│   ├── 04-workflow-configurable.md
│   ├── 05-validate.md                 ← ✅ IMPLEMENTADO
│   ├── 06-init-scaffold.md            ← ✅ IMPLEMENTADO
│   ├── 07-checkpoint-resume.md        ← ✅ IMPLEMENTADO
│   ├── 08-feedback-agentes.md         ← ✅ IMPLEMENTADO
│   ├── 13-groom-generacion-historias.md ← ✅ IMPLEMENTADO
│   ├── 14-groom-from-dir.md
│   └── 15-groom-interactive.md
└── tests/
    └── fixtures/
        ├── story_draft.md
        ├── story_blocked.md
        └── story_business_review.md
```

---

## ⚙️ Comandos esenciales

```bash
# Compilar (debug)
cargo build

# Compilar (release)
cargo build --release

# Ejecutar todos los tests
cargo test

# Ejecutar tests de un módulo específico
cargo test --lib state
cargo test --lib checkpoint
cargo test --lib groom

# Ejecutar test ignorado (requiere pi instalado)
cargo test -- --ignored

# Ver warnings
cargo check

# Formatear
cargo fmt

# Linting
cargo clippy -- -D warnings
```

---

## 🔄 Máquina de estados

### Diagrama del flujo feliz

```
Draft ──PO(groom)──→ Ready ──QA(tests)──→ Tests Ready ──Dev(implement)──→ In Review
                                                                         Reviewer │
                                Done ←──PO(validate)── Business Review
```

### Transiciones canónicas (inmutables, definidas en `state.rs`)

| # | De | A | Actor | Condición |
|---|---|---|---|---|
| 1 | `Draft` | `Ready` | **PO** | Historia cumple DoR |
| 2 | `Ready` | `Tests Ready` | **QA** | Tests escritos para todos los CAs |
| 3 | `Ready` | `Draft` | **QA** (rollback) | Historia no es testeable |
| 4 | `Tests Ready` | `In Review` | **Dev** | Implementación completa |
| 5 | `Tests Ready` | `Tests Ready` | **QA** (corregir) | Dev reporta tests rotos |
| 6 | `In Progress` | `In Review` | **Dev** (fix) | Corrección aplicada |
| 7 | `In Review` | `Business Review` | **Reviewer** | DoD técnico OK |
| 8 | `In Review` | `In Progress` | **Reviewer** | Rechazo técnico |
| 9 | `Business Review` | `Done` | **PO** (validate) | Validación de negocio OK |
| 10 | `Business Review` | `In Review` | **PO** | Rechazo leve |
| 11 | `Business Review` | `In Progress` | **PO** | Rechazo grave |
| 12 | `*` | `Blocked` | **Orchestrator** | Dependencias ≠ Done |
| 13 | `Blocked` | `Ready` | **Orchestrator** | Dependencias pasan a Done |
| 14 | `*` | `Failed` | **Orchestrator** | `max_reject_cycles` agotado |

> ⚠️ Las transiciones 12, 13, 14 son automáticas (sin agente).  
> Las transiciones son **inmutables** — no se añaden en runtime.

### Estados terminales

- `Done` — historia completada exitosamente
- `Failed` — superó `max_reject_cycles`

---

## 📝 Contrato de historia (.md)

Los archivos de historia deben seguir este formato **exacto**:

```markdown
# STORY-NNN: Título

## Status
**<Draft|Ready|Tests Ready|In Progress|In Review|Business Review|Done|Blocked|Failed>**

## Epic
EPIC-XXX

## Descripción
...

## Criterios de aceptación
- [ ] CA1: descripción
- [ ] CA2: ...

## Dependencias       ← opcional
- Bloqueado por: STORY-XXX, STORY-YYY

## Activity Log       ← obligatorio
- YYYY-MM-DD | PO | descripción
```

### Reglas de parseo (`story.rs`)

| Campo | Cómo se extrae |
|-------|---------------|
| **Status** | Busca `## Status` (case-insensitive), lee la línea siguiente, limpia `**...**` |
| **Bloqueadores** | Busca `Bloqueado por:` (case-insensitive) dentro de `## Dependencias`, extrae `STORY-\d+` |
| **Epic** | Busca `## Epic`, lee la línea siguiente, extrae `EPIC-\d+` |
| **Last rejection** | Busca `## Activity Log`, última línea que contiene "rechaz" (case-insensitive) |
| **Last actor** | Busca `## Activity Log`, última línea, extrae actor entre `|` |

---

## 🧩 Descripción de módulos

### `main.rs` — Entry point
- Define `Cli` con clap (16 flags + project_dir posicional)
- Detecta subcomandos `validate`, `init`, `groom`, `help` antes del parseo de clap
- Configura tracing (stderr, env-filter, respeta `--quiet`)
- Salida JSON a stdout con `--json`; exit codes 0/2/3
- Manejo de `--resume` y `--clean-state`

### `config.rs` — Configuración
- `Config` con `#[serde(default)]`: todos los campos tienen defaults razonables
- Carga desde `.regista.toml`; si no existe, usa defaults
- Nuevos campos: `groom_max_iterations` (default 5), `inject_feedback_on_retry` (default true)
- `validate()`: verifica que `stories_dir` existe, crea `decisions_dir` y `log_dir`

### `state.rs` — Máquina de estados
- `Status` enum: 9 variantes
- `Actor` enum: `ProductOwner`, `QaEngineer`, `Developer`, `Reviewer`, `Orchestrator`
- `Transition` struct con `Status::ALL` — 14 transiciones canónicas
- Tests: 23 tests

### `story.rs` — Parseo de historias
- `Story` struct: `id`, `path`, `status`, `epic`, `blockers`, `last_rejection`, `raw_content`
- `load()`: lee archivo .md y parsea todos los campos
- `set_status()`: escribe a disco con backup atómico + verificación
- `advance_status_in_memory()`: muta estado sin tocar disco (dry-run)
- `last_actor()`: extrae último actor del Activity Log
- Tests: 12 tests

### `dependency_graph.rs` — Grafo de dependencias
- `DependencyGraph`: `forward`, `reverse`, DFS con colores
- `blocks_count()`, `has_cycle_from()`, `has_any_cycle()`, `find_cycle_members()`
- Tests: 4 tests

### `deadlock.rs` — Detección de bloqueos
- `DeadlockResolution` enum: `NoDeadlock`, `InvokePoFor`, `PipelineComplete`
- `analyze()`: algoritmo de 4 pasos con priorización
- Prioriza por: mayor `unblocks`, luego menor ID numérico
- Tests: 7 tests

### `agent.rs` — Invocación de agentes con feedback
- `invoke_with_retry()`: loop con backoff exponencial, acepta `AgentOptions`
- `AgentOptions`: `story_id`, `decisions_dir`, `inject_feedback`
- Feedback rico: guarda stdout/stderr en `decisions/`, inyecta errores en reintentos
- `AgentResult` incluye `attempt` y `attempts: Vec<AttemptTrace>`
- Tests: 3 tests + 1 ignorado

### `prompts.rs` — Generación de prompts
- `PromptContext`: `story_id`, `stories_dir`, `decisions_dir`, `last_rejection`, `from`, `to`
- 7 funciones de prompt (una por transición accionable por agentes)
- Todos los prompts terminan con `"NO preguntes. 100% autónomo."`
- Tests: 4 tests

### `orchestrator.rs` — Loop principal
- `run()`: dispatch a `run_real()` o `run_dry()` según `options.dry_run`
- `run_real()`: loop con carga de historias, transiciones automáticas, deadlock, process_story
  - Acepta `resume_state: Option<OrchestratorState>` para `--resume`
  - Guarda checkpoint tras cada `process_story()` exitoso
  - Limpia checkpoint en `PipelineComplete`
- `run_dry()`: simulación en memoria sin agentes ni escritura a disco
  - Usa `advance_status_in_memory()` para mutar estados
  - Muestra desbloqueos y estima tiempo
- `process_story()`: determina skill + prompt, invoca agente con AgentOptions
- `filter_stories()`: aplica filtros `--story`, `--epic`, `--epics`
- Tests: 18 tests

### `checkpoint.rs` — Persistencia del estado
- `OrchestratorState`: `iteration`, `reject_cycles`, `story_iterations`, `story_errors`
- `save()` / `load()` / `remove()` sobre `.regista.state.toml`
- `load()` maneja archivos corruptos (los borra)
- Tests: 5 tests

### `validator.rs` — Comando `validate`
- Valida: config, skills, historias (parseo, Activity Log), dependencias (refs rotas, ciclos), git
- `ValidationResult` con `Vec<Finding>` (severity: Error/Warning, category, message)
- `--json` para CI, exit codes: 0=OK, 1=errores, 2=warnings
- Tests: 3 tests

### `init.rs` — Comando `init`
- Genera `.regista.toml`, 4 `SKILL.md`, estructura `product/...`
- `--light`: solo config, sin skills
- `--with-example`: incluye historia y épica de ejemplo
- No pisa archivos existentes
- Tests: 5 tests

### `groom.rs` — Comando `groom`
- `run()`: invoca al PO para generar historias desde spec
- **Bucle de validación**: groom → validate dependencias → si errores → feedback al PO → repetir
- Máx iteraciones: `groom_max_iterations` (default 5)
- `--max-stories` (0 = sin límite), `--merge`/`--replace`
- Prompts: `groom_prompt_initial()`, `groom_prompt_fix()` con `GroomCtx`
- Tests: 6 tests

### `hooks.rs` — Hooks post-fase
- `run_hook()`: ejecuta `sh -c "<comando>"`, retorna error si exit code ≠ 0

### `git.rs` — Snapshots y rollback
- `snapshot()`: `git add -A && git commit -q -m "snapshot: {label}"`
- `rollback()`: `git reset --hard <hash>`
- Auto-inicializa repo si `git.enabled = true` y no existe

### `daemon.rs` — Modo daemon
- `detach()`: spawnea proceso hijo con `--daemon` interno
- `status()`, `kill()`, `follow()`: gestión del proceso
- Estado guardado en `.regista.pid` (TOML)
- Tests: 6 tests

---

## 💡 Decisiones de diseño importantes

1. **Agnóstico al proyecto anfitrión**: regista no sabe de Rust, cargo, ni nada.  
   Solo invoca `pi --skill <path>` con prompts genéricos.

2. **Workflow fijo e inmutable**: las 14 transiciones son canónicas.  
   No se añaden transiciones en runtime **por diseño**.

3. **Subcomandos vía args manual**: `validate`, `init`, `groom` se detectan antes de clap  
   inspeccionando `std::env::args()`. Evita refactorizar toda la CLI con clap subcommands.

4. **Dry-run en memoria**: `advance_status_in_memory()` muta `Story` sin tocar el filesystem.  
   Permite simular pipelines completos sin gastar créditos de LLM.

5. **Checkpoint TOML**: el estado del orquestador se guarda en `.regista.state.toml`.  
   Si el pipeline se interrumpe, `--resume` lo reanuda. Se limpia en `PipelineComplete`.

6. **Feedback rico en reintentos**: cuando un agente falla, su stderr se guarda en  
   `decisions/` y se inyecta en el prompt del reintento. Truncado a 2000 bytes.

7. **Groom con bucle validate**: generar historias no basta — hay que validar que las  
   dependencias son correctas. El PO recibe feedback concreto y corrige en bucle.

8. **Salida JSON dual**: `--json` emite JSON a stdout, logs a stderr.  
   `--quiet` suprime logs. Compatible con `regista --json > report.json`.

9. **Backoff exponencial**: `agent.rs` duplica el delay entre reintentos (`delay *= 2`).

10. **`set_status()` con backup atómico**: escribe → re-parsea → si falla, restaura `.bak`.

---

## 🚧 Pendiente (roadmap)

### Features no implementadas

| # | Feature | Esfuerzo |
|---|---------|----------|
| 01 | Paralelismo | Alto |
| 04 | Workflow configurable | Medio |
| 09 | Prompts agnósticos al stack | Bajo |
| 10 | Cross-story context | Medio |
| 11 | TUI / dashboard | Medio |
| 12 | Cost tracking | Medio |
| 14 | Groom `--from-dir` | Bajo |
| 15 | Groom `--interactive` | Medio |

---

## 🧪 Estrategia de testing

- **Tests unitarios**: cada módulo tiene `#[cfg(test)] mod tests` con fixtures inline
- **Fixtures**: `tests/fixtures/` contiene archivos .md de ejemplo para pruebas de parseo
- **Test ignorado**: `agent::tests::invoke_with_retry_fails_when_pi_not_installed` (requiere `pi` en PATH)
- **Total**: 104 tests pasando, 0 fallos, 1 ignorado

Para añadir tests:
- Usa `make_story()` o `story_fixture()` helpers para crear Stories sintéticas
- No dependas de archivos reales salvo en tests de `story.rs` (que usan fixtures)
- Para tests de nuevos módulos, usa `tempfile::tempdir()` para aislar el filesystem

---

## ⚠️ Errores comunes y anti-patrones

- **Añadir transiciones a `Status::ALL`**: rompe la inmutabilidad del workflow.  
  Las 14 transiciones son el contrato fijo.

- **Parsear historias sin usar `extract_section()`**: usa las funciones existentes en `story.rs`.

- **Modificar `raw_content` sin actualizar `status`**: deben estar siempre sincronizados.

- **Ejecutar hooks sin `sh -c`**: los hooks son comandos shell.

- **Asumir que todos los bloqueadores existen**: filtrar siempre con `status_map.get()`.

- **Usar `..ctx` (struct update) sin clonar**: `PromptContext` contiene Strings, el update  
  syntax los mueve. Si necesitas reutilizar `ctx`, clona los campos explícitamente.

- **Llamar a `invoke_with_retry` sin `AgentOptions`**: la firma cambió, ahora requiere  
  el 4º argumento. Usa `&AgentOptions::default()` si no necesitas feedback.

---

## 🔑 Convenciones de código

- **Idioma**: código y comentarios en español, nombres de variables/funciones en inglés
- **Formato**: `cargo fmt` (rustfmt estándar)
- **Documentación**: `//!` para módulos, `///` para items públicos
- **Errores**: `anyhow::Result<T>` y `anyhow::bail!()` (nunca `unwrap()` en lógica de negocio)
- **Logging**: `tracing::info!()` / `warn!()` / `error!()` / `debug!()` (nunca `println!`)
- **Regex estáticos**: usa `LazyLock<Regex>` para compilar una sola vez
- **Defaults de serde**: `#[serde(default)]` + funciones `default_xxx()`
- **Tests**: usa `assert!()` / `assert_eq!()` con mensajes descriptivos
- **Nuevos módulos**: siguen el patrón `pub fn run(...) -> anyhow::Result<...>` para su entry point