Skip to content

Commit 20d208a

Browse files
committed
feat: add built-in playbooks for various stacks and enhance skill management
- Introduced built-in playbooks for Node.js, Python, React, and Spring Boot to streamline common tasks and improve user guidance. - Enhanced the skill management system by allowing the extension of skills from built-in playbooks based on the active focus. - Added documentation for creating new skills, hooks, tools, and modifying the harness to improve developer experience. - Implemented a new focus pack addition process to support new stacks and languages.
1 parent 3dba7a4 commit 20d208a

13 files changed

Lines changed: 478 additions & 4 deletions

skills/agregar-focus-pack.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
name: agregar un focus pack de un stack nuevo
3+
focus: dpx
4+
cuando: "USAR cuando: agregar soporte para un stack/lenguaje nuevo (un focus pack: Go, Kotlin, Vue, etc.). NO usar para skills sueltos ni para tocar un pack que ya existe."
5+
---
6+
Un focus pack es el conocimiento de dominio que se inyecta al system prompt según
7+
el stack. Para añadir uno (p.ej. `go`):
8+
9+
1. **Archivo** `src/focus/go.rs` con `pub const SKILLS: &str = "..."` — el dominio.
10+
Incluye SIEMPRE un bloque de VERSIONES ACTUALES (autoritativo, junio 2026) que
11+
gane sobre la memoria del modelo; no inventes números de versión.
12+
2. **Declara el módulo**: `mod go;` en `src/focus/mod.rs` (junto a los otros).
13+
3. **Catálogo**: añade su `Focus { id, name, tagline }` en `catalog()` (el `id` es
14+
lo que el usuario elige, p.ej. `"go"`).
15+
4. **Inyección**: añade el caso a `domain_skills()` (`"go" => Some(go::SKILLS)`).
16+
5. **Detección** (opcional): enséñale a `fs::detect_stack` a reconocer el stack por
17+
sus archivos raíz (p.ej. `go.mod`).
18+
6. **Built-in playbooks** (opcional pero recomendado): añade `pub const PLAYBOOKS`
19+
al pack y su caso en `focus::builtin_playbooks` (ver skill "crear un skill").
20+
7. **Verifica** clippy estricto + tests; reinstala con `/actualizar` (el prompt se
21+
compila en el binario).

skills/agregar-hook.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
name: agregar un evento de hook del ciclo de vida
3+
focus: dpx
4+
cuando: "USAR cuando: agregar un hook o evento del ciclo de vida (OnSessionStart, PreToolUse y similares) que dispare comandos del usuario. NO usar para tools del agente ni comandos del REPL."
5+
---
6+
Los hooks viven en `src/cli/hooks.rs` y se configuran en `.dpx/hooks.toml`. Para
7+
añadir un evento nuevo:
8+
9+
1. **Variante del enum**: añade el caso a `enum HookEvent` en `cli/hooks.rs`.
10+
2. **Mapeo string ↔ enum**: añádelo en `HookEvent::parse` (string → variante) y en
11+
la conversión inversa (variante → string). Deben coincidir EXACTO con el valor
12+
que el usuario escribe en `hooks.toml`.
13+
3. **Disparo**: llama a `run_hooks(&hooks, &HookEvent::TuEvento, ...)` en el punto
14+
del ciclo de vida donde debe dispararse (mira cómo se dispara `OnSessionStart`
15+
al arrancar la sesión en `src/cli/chat.rs`).
16+
4. **Doc**: actualiza el comentario de cabecera de `hooks.rs` (lista de eventos) y
17+
el README si menciona los hooks.
18+
5. **Test** de `parse`/round-trip del nuevo evento + clippy estricto + tests.

skills/agregar-tool.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
name: agregar una tool/acción del agente
3+
focus: dpx
4+
cuando: "USAR cuando: agregar una acción o tool nueva que dpx pueda emitir (un bloque dpx:algo que lee o muta el repo). NO usar para comandos del REPL (/x) ni paneles."
5+
---
6+
Las acciones de dpx son bloques con marcador (`dpx:write path=`, `dpx:read path=`,
7+
`dpx:run`, `dpx:edit`, `dpx:delete`, `dpx:search`). Para añadir una:
8+
9+
1. **Parser** en `src/fs/mod.rs`: una función `parse_<x>_marker`/`is_<x>_fence`
10+
estilo las existentes (`parse_path_marker`, `is_run_fence`).
11+
2. **Guard de stripping**: añade tu marcador a la lista de `on_fence`/`on_next` en
12+
`fs/mod.rs` (la que limpia los bloques de acción del texto visible) — si no, tu
13+
bloque se imprime crudo.
14+
3. **Frontera lectura/mutación** (CRÍTICO): si la tool LEE, va libre. Si MUTA
15+
(write/edit/delete/run), cabléala por la puerta de confirmación en
16+
`src/cli/chat.rs` (`process_writes`/`process_edits`/`process_deletes`/`confirm_run`)
17+
y respeta los guards (shrink, big-rewrite, sandbox). NUNCA un atajo que mute en silencio.
18+
4. **Doctrina**: documenta el marcador en el prompt de herramientas (`SHARED_TOOLS`
19+
en `src/focus/mod.rs`) para que dpx sepa que existe.
20+
5. **Test** del parser (camino feliz + borde) y verifica con clippy estricto + tests.

skills/crear-skill.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
name: crear un skill / playbook para dpx
3+
focus: dpx
4+
cuando: "USAR cuando: crear, escribir o agregar un skill o playbook nuevo (curado o built-in de un stack: React, Python, Node, etc.), o cuando falte un playbook para una tarea que se repite."
5+
---
6+
Un skill es un PLAYBOOK A→B: le dice a dpx los pasos exactos de una tarea que se
7+
repite, para que no explore a ciegas ni dé algo genérico. Hay dos tipos:
8+
9+
## A) Curado local (`skills/*.md`) — para ESTE repo
10+
Crea `skills/<nombre-kebab>.md` con frontmatter + cuerpo:
11+
```
12+
---
13+
name: <título corto y reconocible>
14+
focus: <id del stack o "dpx">
15+
cuando: "USAR cuando: <frases gatillo concretas>. NO usar para <contraejemplo>."
16+
---
17+
1. <paso A→B con la RUTA/función real>
18+
2. ...
19+
```
20+
21+
## B) Built-in por stack (viene en dpx, para los USUARIOS)
22+
Añade una tupla al `pub const PLAYBOOKS` del focus pack (p.ej. `src/focus/react.rs`):
23+
`("nombre", "USAR cuando: …", "1. paso\n2. paso")`. Si el pack aún no tiene
24+
`PLAYBOOKS`, créalo (copia la forma del de `spring_boot.rs`) y añade su caso a
25+
`focus::builtin_playbooks` en `src/focus/mod.rs`. Reinstala para que tome efecto.
26+
27+
## Reglas de un BUEN skill (esto es lo que evita lo genérico)
28+
- El `cuando` ES el gatillo: ponlo INSISTENTE, con frases y palabras reales que el
29+
usuario diría (dpx tiende a sub-disparar los skills). Incluye un "NO usar para…".
30+
- Cuerpo CORTO y ESPECÍFICO: rutas, funciones, anotaciones, comandos REALES del
31+
stack — nada de "crea una clase y añade lógica". Si no es específico, no sirve.
32+
- **Investiga antes de escribir** un stack que no domines: confirma las VERSIONES y
33+
convenciones ACTUALES (busca en la web si hace falta; alinéate con el bloque de
34+
versiones del focus pack). NUNCA inventes versiones ni APIs.
35+
- Un solo playbook por tarea repetible; pasos numerados que terminen en "verifica".

skills/escribir-tests-dpx.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
name: escribir tests en el repo de dpx
3+
focus: dpx
4+
cuando: "USAR cuando: escribir o arreglar un test de dpx, testear el loop del chat, mockear el modelo, o probar estado global (atómicos)."
5+
---
6+
Convenciones de testabilidad de dpx que DEBES respetar (romperlas hace tests
7+
frágiles o imposibles):
8+
9+
1. **Costura del loop**: `run_turn` toma `&impl TurnBrain` (no `&Mentor`) y
10+
`ask: &mut dyn FnMut(&str) -> Option<String>` (no el editor directo). En los
11+
tests usa `FakeMentor` para guionar las `ChatReply` y un `ask` que da respuestas
12+
fijas. Si cambias la firma del loop, MANTÉN esta costura.
13+
2. **Estado global = atómicos** (`ui::CANCEL`, `BUDGET`, el ledger de `token.rs`, el
14+
`State` de `checkpoint.rs`): NUNCA los toques desde un test (carreras entre
15+
tests). Prueba la lógica en una INSTANCIA LOCAL — copia el patrón de los tests de
16+
`token.rs`/`checkpoint.rs`.
17+
3. **Cada lógica nueva lleva test** en su mismo módulo: camino feliz + un borde.
18+
Funciones de parsing/formato son las más fáciles y valiosas de cubrir.
19+
4. **Verifica de verdad**: `cargo clippy --all-targets -- -D warnings` Y
20+
`cargo test`. clippy/test NO cazan bugs visuales — razona la salida real.
21+
5. Tests temporales que crean archivos: usa rutas en `std::env::temp_dir()` con el
22+
pid, y límpialas al final.

skills/modificar-harness.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
name: modificar el harness (doctrina/prompt de dpx)
3+
focus: dpx
4+
cuando: "USAR cuando: agregar o cambiar una regla, lección o doctrina del comportamiento de dpx (el system prompt / focus pack), o corregir una falla recurrente del agente."
5+
---
6+
El "harness" es lo que define cómo se comporta dpx. Va compilado en el binario, así
7+
que un cambio NO surte efecto hasta reinstalar.
8+
9+
1. **¿Dónde va la regla?**
10+
- Doctrina GENERAL (aplica a todos los stacks): `src/focus/mod.rs` (las secciones
11+
de método: economía de rondas, cambio mínimo, errores recurrentes…).
12+
- Lección ESPECÍFICA de trabajar sobre dpx: `src/focus/dpx.rs`.
13+
- Conocimiento de un stack: el focus pack correspondiente (`spring_boot.rs`, etc.).
14+
2. **Sé concreto y corto**: una regla imperativa con el porqué y el síntoma real que
15+
evita ("pasó de verdad: …"). Las reglas vagas se ignoran.
16+
3. **No dupliques**: busca si ya existe una regla parecida y refínala en vez de
17+
añadir una quinta que diga lo mismo.
18+
4. **Reinstala**: `cargo install --path . --force` (o el usuario corre `/actualizar`).
19+
OJO: falla si hay un dpx corriendo (binario en uso, os error 5 en Windows).
20+
5. **Valida** con el banco de pruebas (`eval/run-eval.sh`): corre la tarea que picaba
21+
la falla y confirma que ahora la caza.

src/agent_skill.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,23 @@ pub struct AgentSkill {
3636
pub vector: Vec<f32>,
3737
}
3838

39-
/// Catálogo de skills curados cargado de `skills/*.md`. De solo lectura: no se
40-
/// auto-persiste nada (los `.md` se editan a mano).
39+
impl AgentSkill {
40+
/// Crea un skill EMPOTRADO (built-in) desde literales de un focus pack. Estos
41+
/// vienen DENTRO de dpx (por stack), así un usuario obtiene playbooks expertos
42+
/// sin escribir nada. El vector se llena perezosamente, igual que los curados.
43+
pub fn builtin(name: &str, when: &str, body: &str, focus: &str) -> Self {
44+
Self {
45+
name: name.to_string(),
46+
body: body.to_string(),
47+
focus: focus.to_string(),
48+
when: when.to_string(),
49+
vector: Vec::new(),
50+
}
51+
}
52+
}
53+
54+
/// Catálogo de skills cargado de `skills/*.md` (curados) + los empotrados del
55+
/// stack activo. De solo lectura: no se auto-persiste nada.
4156
pub struct SkillBook {
4257
skills: Vec<AgentSkill>,
4358
}
@@ -64,6 +79,16 @@ impl SkillBook {
6479
self.skills.iter().collect()
6580
}
6681

82+
/// Añade skills (p.ej. los empotrados del focus activo). No duplica por nombre
83+
/// — un skill curado de `skills/` con el mismo nombre gana sobre el built-in.
84+
pub fn extend(&mut self, more: Vec<AgentSkill>) {
85+
for s in more {
86+
if !self.skills.iter().any(|e| e.name == s.name) {
87+
self.skills.push(s);
88+
}
89+
}
90+
}
91+
6792
/// Embebe (vectoriza) los skills que aún no tienen vector, usando `embed`.
6893
/// Se llama perezosamente la primera vez que se busca, cuando el motor de
6994
/// embeddings ya está cargado — así no encarecemos el arranque.
@@ -204,6 +229,21 @@ mod tests {
204229
assert_eq!(hits[0].name, "endpoint");
205230
}
206231

232+
#[test]
233+
fn extend_no_duplica_y_el_curado_gana() {
234+
// Un curado de skills/ con el mismo nombre debe ganar sobre el built-in.
235+
let mut book = SkillBook {
236+
skills: vec![AgentSkill::builtin("crear endpoint", "w", "curado", "spring-boot")],
237+
};
238+
book.extend(vec![
239+
AgentSkill::builtin("crear endpoint", "w", "builtin", "spring-boot"), // mismo nombre → ignorado
240+
AgentSkill::builtin("otro", "w", "nuevo", "spring-boot"), // nombre nuevo → entra
241+
]);
242+
assert_eq!(book.len(), 2, "no duplica por nombre");
243+
let endpoint = book.ranked().into_iter().find(|s| s.name == "crear endpoint").unwrap();
244+
assert_eq!(endpoint.body, "curado", "el primero (curado) gana");
245+
}
246+
207247
#[test]
208248
fn embed_pending_llena_solo_los_vacios() {
209249
let mut book = SkillBook { skills: Vec::new() };

src/cli/chat.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,17 @@ pub async fn run(
152152
let mut mem_store = crate::memory::MemoryStore::load(store.dpx_dir());
153153
let mut embedder: Option<crate::memory::Embedder> = None;
154154

155-
// Skills CURADOS del proyecto (playbooks `skills/*.md`). Como la memoria, se
156-
// cargan barato (solo leer los .md); el embedder solo si de verdad se usan.
155+
// Skills del proyecto: CURADOS (`skills/*.md`, locales) + EMPOTRADOS del stack
156+
// activo (vienen en dpx). Carga barata; el embedder solo si de verdad se usan.
157157
let mut skillbook = crate::agent_skill::SkillBook::from_dir(&cwd.join("skills"));
158+
skillbook.extend(
159+
crate::focus::builtin_playbooks(focus_id.as_deref())
160+
.iter()
161+
.map(|(n, w, b)| {
162+
crate::agent_skill::AgentSkill::builtin(n, w, b, focus_id.as_deref().unwrap_or(""))
163+
})
164+
.collect(),
165+
);
158166

159167
loop {
160168
// De vuelta en el prompt: la pestaña muestra dpx en reposo.

src/focus/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,20 @@ fn domain_skills(focus_id: &str) -> Option<&'static str> {
191191
}
192192
}
193193

194+
/// Playbooks EMPOTRADOS (built-in) del stack activo: `(nombre, cuándo, pasos)`.
195+
/// Vienen DENTRO de dpx, así un usuario obtiene playbooks A→B expertos sin
196+
/// escribir skills. Se cargan junto a los curados de `skills/` (el CLI los
197+
/// recupera por similitud antes del turno). Vacío = sin playbooks para ese stack.
198+
pub fn builtin_playbooks(focus_id: Option<&str>) -> &'static [(&'static str, &'static str, &'static str)] {
199+
match focus_id {
200+
Some("spring-boot") => spring_boot::PLAYBOOKS,
201+
Some("react") => react::PLAYBOOKS,
202+
Some("node") => node::PLAYBOOKS,
203+
Some("python") => python::PLAYBOOKS,
204+
_ => &[],
205+
}
206+
}
207+
194208
/// Devuelve el nombre legible de un focus pack (para banners).
195209
/// `None` = mentor general, sin enfoque de stack.
196210
pub fn display_name(focus_id: Option<&str>) -> &str {

0 commit comments

Comments
 (0)