navori 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/README.md +15 -1
  2. package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +15 -9
  3. package/dist/assets/core/core-assets/lib-skills/formik.md +57 -0
  4. package/dist/assets/core/core-assets/lib-skills/joi-validation.md +73 -0
  5. package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/mongoose.md +3 -2
  6. package/dist/assets/core/core-assets/lib-skills/redux-toolkit.md +59 -0
  7. package/dist/assets/core/core-assets/lib-skills/socketio.md +56 -0
  8. package/dist/assets/core/core-assets/lib-skills/tanstack-query.md +58 -0
  9. package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
  10. package/dist/assets/core/core-assets/managed/idioma-rol.md +1 -1
  11. package/dist/assets/core/core-assets/managed/operaciones-seguras.md +1 -0
  12. package/dist/assets/core/core-assets/presets/background-worker/managed/stack.md +18 -0
  13. package/dist/assets/core/core-assets/presets/background-worker/skills/job-scheduling.md +54 -0
  14. package/dist/assets/core/core-assets/presets/background-worker/skills/queue-consumers.md +55 -0
  15. package/dist/assets/core/core-assets/presets/background-worker/skills/worker-lifecycle.md +57 -0
  16. package/dist/assets/core/core-assets/presets/background-worker.json +39 -0
  17. package/dist/assets/core/core-assets/presets/express-mongoose/managed/stack.md +2 -2
  18. package/dist/assets/core/core-assets/presets/express-mongoose/skills/pr-create.md +18 -18
  19. package/dist/assets/core/core-assets/presets/express-mongoose.json +0 -12
  20. package/dist/assets/core/core-assets/settings/settings-base.json +22 -1
  21. package/dist/index.js +797 -346
  22. package/package.json +4 -3
  23. /package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/zod-validation.md +0 -0
package/README.md CHANGED
@@ -68,6 +68,7 @@ Un preset aporta skills y reglas específicas del stack además del core. El `in
68
68
  | `nextjs` | Next.js (App Router) |
69
69
  | `nestjs` | NestJS (backend) |
70
70
  | `express-mongoose` | Express + Mongoose (backend) |
71
+ | `background-worker` | Worker de fondo (jobs + colas: agenda / bullmq / amqplib) |
71
72
  | `astro` | Astro (static / SSR) |
72
73
  | `medusa` | Medusa.js v2 (backend) |
73
74
 
@@ -135,6 +136,18 @@ navori ticket show bonum BNM-123
135
136
  El workspace también guarda defaults heredables:
136
137
  ```bash
137
138
  navori init --workspace bonum # hereda engines, plugins, branchBase, etc.
139
+
140
+ # Ajustar un default sin editar el manifest a mano
141
+ navori workspace set-default bonum branchBase main
142
+ navori workspace set-default bonum prTarget develop # PRs van a develop, no a main
143
+ navori workspace set-default bonum engines claude,cursor
144
+ navori workspace set-default bonum plugins.engram.enabled true
145
+ ```
146
+
147
+ Y re-renderizar todos los repos del workspace de una vez:
148
+ ```bash
149
+ navori workspace render bonum # preview (no toca disco)
150
+ navori workspace render bonum --apply # escribe en cada repo
138
151
  ```
139
152
 
140
153
  Storage: `~/.navori/workspaces/<name>/` (manifest + tickets/ + backups/).
@@ -172,7 +185,8 @@ navori configure plugins # multiselect de plugins activos
172
185
  navori configure quality-gate # nuevo comando de quality gate
173
186
  navori configure language en # switch a inglés (fallback a es)
174
187
  navori configure engines # multiselect: claude / agents-md / cursor / copilot
175
- navori configure branch-base develop # fijar la branch base
188
+ navori configure branch-base main # punto de fork / rama protegida
189
+ navori configure pr-target develop # rama destino del PR (gh pr create --base)
176
190
  navori configure workspace bonum # asociar a un workspace
177
191
  ```
178
192
 
@@ -18,20 +18,22 @@ Te encargas del **cierre del ciclo**: commits Conventional bien estructurados y
18
18
  ## Cuándo NO activar
19
19
 
20
20
  - Working tree con cambios sin commitear cuando el usuario solo pidió "abre el PR" → primero commiteas o pides permiso.
21
- - Estás en `{{branchBase}}` u otra rama protegida → abort + pedir branch.
21
+ - Estás en `{{branchBase}}` o `{{prTarget}}` u otra rama protegida → abort + pedir branch.
22
22
  - Harness activo y `.claude/progress/review_*.md` reciente contiene `CHANGES_REQUESTED` → no se crea PR.
23
23
  - Quality gate en rojo en este turno.
24
24
 
25
+ > **Dos ramas, dos roles:** `{{branchBase}}` es el punto de fork (de dónde ramificaste). `{{prTarget}}` es la rama destino del PR (`gh pr create --base`). Suelen coincidir; cuando difieren, el PR y su diff se calculan contra `{{prTarget}}`.
26
+
25
27
  ## Pre-flight obligatorio
26
28
 
27
29
  Corre estos chequeos antes de redactar nada. Si algo falla, paras y reportas.
28
30
 
29
31
  ```bash
30
32
  git status --porcelain # qué falta commitear
31
- git rev-parse --abbrev-ref HEAD # no puede ser {{branchBase}}
32
- git fetch origin {{branchBase}} --quiet
33
- git log origin/{{branchBase}}..HEAD --oneline # debe haber ≥1 commit (o cambios para commitear)
34
- git diff origin/{{branchBase}}...HEAD --stat # scope del PR
33
+ git rev-parse --abbrev-ref HEAD # no puede ser {{branchBase}} ni {{prTarget}}
34
+ git fetch origin {{prTarget}} --quiet
35
+ git log origin/{{prTarget}}..HEAD --oneline # debe haber ≥1 commit (o cambios para commitear)
36
+ git diff origin/{{prTarget}}...HEAD --stat # scope REAL del PR (contra el target)
35
37
  gh auth status # gh autenticado
36
38
  ```
37
39
 
@@ -59,10 +61,11 @@ Sin `APPROVED` y con harness activo → abort, dile al usuario que falta review.
59
61
 
60
62
  ## Flujo de PR
61
63
 
62
- 1. **Recopilar contexto** (curado, no volcar todo el repo):
63
- - `git log origin/{{branchBase}}..HEAD --oneline` — commits incluidos.
64
- - `git diff origin/{{branchBase}}...HEAD --stat` — siempre.
65
- - `git diff origin/{{branchBase}}...HEAD` — solo si el diff < 500 líneas. Si es mayor, usa solo el stat + lista de archivos + los hunks de los 2–3 archivos más relevantes.
64
+ 1. **Recopilar contexto** (curado, no volcar todo el repo). El diff del PR es contra `{{prTarget}}` (lo que GitHub mostrará):
65
+ - `git log origin/{{prTarget}}..HEAD --oneline` — commits incluidos.
66
+ - `git diff origin/{{prTarget}}...HEAD --stat` — siempre.
67
+ - `git diff origin/{{prTarget}}...HEAD` — solo si el diff < 500 líneas. Si es mayor, usa solo el stat + lista de archivos + los hunks de los 2–3 archivos más relevantes.
68
+ - **Arrastre de commits** (solo si `{{branchBase}}` ≠ `{{prTarget}}`): `git fetch origin {{branchBase}} --quiet` y `git rev-list --count origin/{{prTarget}}..origin/{{branchBase}}`. Si es > 0, `{{branchBase}}` va adelantado de `{{prTarget}}` y tu PR arrastra esos commits ajenos: avisa al usuario y sugiere rebasar sobre `{{prTarget}}` antes de abrir.
66
69
  - Ticket si aplica: nombre del branch (ej. `BT-1234-fix-x` → `BT-1234`) o referencia en el primer commit.
67
70
  - `.claude/progress/impl_<feature>.md` si existe — decisiones no obvias.
68
71
 
@@ -79,6 +82,7 @@ Sin `APPROVED` y con harness activo → abort, dile al usuario que falta review.
79
82
 
80
83
  ```bash
81
84
  gh pr create \
85
+ --base {{prTarget}} \
82
86
  --title "<title validado>" \
83
87
  --body "$(cat <<'EOF'
84
88
  <body validado>
@@ -86,6 +90,8 @@ Sin `APPROVED` y con harness activo → abort, dile al usuario que falta review.
86
90
  )"
87
91
  ```
88
92
 
93
+ Siempre pasa `--base {{prTarget}}` explícito — no dejes que `gh` use la rama default del repo. Si el target cambió, ajústalo con `navori configure pr-target`.
94
+
89
95
  5. **Output al usuario**: solo la URL del PR + 1 línea con el título. Nada más.
90
96
 
91
97
  ## Template del body (default genérico)
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: formik
3
+ description: Patrones de Formik en React+TS — schema de validación, estado de form, submit, errores por campo. Aplica al crear o tocar formularios con Formik.
4
+ type: reference
5
+ ---
6
+
7
+ # Formik — convenciones
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al crear o tocar un formulario con Formik: definir initial values, validación, submit, o pintar errores. Formik es la fuente de verdad del estado del form — no dupliques sus valores en `useState` paralelos.
12
+
13
+ ## El patrón
14
+
15
+ Validación declarativa con un schema (Yup/Zod), no a mano en `validate`:
16
+
17
+ ```ts
18
+ const formik = useFormik({
19
+ initialValues: { email: '', role: 'coachee' },
20
+ validationSchema: toFormikValidationSchema(loginSchema), // zod, o un Yup schema
21
+ onSubmit: async (values, { setSubmitting, setStatus }) => {
22
+ try {
23
+ await api.login(values);
24
+ } catch (err) {
25
+ setStatus(toFormError(err)); // error de servidor, no de campo
26
+ } finally {
27
+ setSubmitting(false);
28
+ }
29
+ },
30
+ });
31
+ ```
32
+
33
+ En el JSX: `formik.getFieldProps('email')` cablea value/onChange/onBlur; el error se muestra solo si el campo fue tocado: `formik.touched.email && formik.errors.email`.
34
+
35
+ ## Gotchas que muerden
36
+
37
+ - **Una sola fuente de verdad.** Los valores viven en Formik; nada de `useState` espejo que se desincroniza.
38
+ - **`touched` antes de mostrar error** — pintar errores antes del primer blur frustra al usuario; usa `touched.<campo> && errors.<campo>`.
39
+ - **`isSubmitting`** deshabilita el botón y evita doble submit; resetéalo siempre en `finally`.
40
+ - **Errores de campo vs de servidor**: un fallo de validación va a `errors` (vía schema); un fallo de API va a `setStatus`/`setFieldError`, no se inventa como error de campo.
41
+ - **Forms grandes** re-renderizan todo en cada tecla; aísla con `<Field>`/componentes memoizados si pesa.
42
+ - **`enableReinitialize`** cuando los initial values llegan async (editar un recurso cargado), o el form arranca vacío.
43
+
44
+ ## Reglas duras
45
+
46
+ 1. Validación en un schema (Yup/Zod), nunca lógica suelta en `validate` inline.
47
+ 2. Estado del form solo en Formik; sin `useState` paralelos a sus valores.
48
+ 3. Error de campo solo tras `touched`; error de servidor vía `setStatus`/`setFieldError`.
49
+ 4. `isSubmitting` controla el botón y se limpia en `finally`.
50
+ 5. `enableReinitialize` para formularios de edición con carga async.
51
+
52
+ ## Antes de declarar listo
53
+
54
+ - El form valida contra un schema y muestra errores solo tras blur.
55
+ - El submit deshabilita el botón y maneja el error de servidor aparte.
56
+ - No hay estado del form duplicado fuera de Formik.
57
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: joi-validation
3
+ description: Validación de input con Joi (@hapi/joi) en Express — schemas por recurso, middleware validate genérico, DTOs explícitos. Aplica al crear schemas o tocar input validation de body/query/params.
4
+ type: reference
5
+ ---
6
+
7
+ # Joi Validation — el patrón canónico
8
+
9
+ Un schema por recurso (`<resource>.schema.ts`), validado por un middleware genérico que reemplaza `req[target]` con el valor convertido. Como Joi no infiere tipos, el DTO es una `interface` explícita junto al schema.
10
+
11
+ ## Cuándo usar este skill
12
+
13
+ Al crear un schema, agregar validación a un endpoint, definir un DTO, o tocar input de body/query/params.
14
+
15
+ ## El patrón
16
+
17
+ El middleware vive en `helpers/validate.ts`: corre `schema.validate(req[target], { abortEarly: true, convert: true })`, y al fallar lanza `BadRequestError(\`${detail.path.join('.')}: ${detail.message}\`)` con el primer issue. En éxito, reasigna `req[target] = value` (el valor ya convertido). Schema y DTO:
18
+
19
+ ```ts
20
+ const objectId = Joi.string().pattern(/^[a-f\d]{24}$/i).message('Invalid ObjectId');
21
+
22
+ export const createResourceSchema = Joi.object({
23
+ owner: objectId.required(),
24
+ resourceType: Joi.string().valid(...Object.values(ResourceTypeEnum)).required(),
25
+ page: Joi.number().integer().positive().default(1),
26
+ });
27
+ export const updateResourceSchema = createResourceSchema.fork(
28
+ Object.keys(createResourceSchema.describe().keys),
29
+ (s) => s.optional(),
30
+ );
31
+
32
+ export interface CreateResourceDto {
33
+ owner: string;
34
+ resourceType: ResourceTypeEnum;
35
+ page: number;
36
+ }
37
+ ```
38
+
39
+ En la route: `router.post('/', validate(createResourceSchema, 'body'), ...)`. En el controller el cast `req.body as CreateResourceDto` es seguro porque el middleware ya validó y convirtió.
40
+
41
+ ## Gotchas que muerden
42
+
43
+ - **ObjectId pelado** (`Joi.string()`) deja pasar `"abc"`; Mongoose lanza CastError 500 en vez de 400 limpio. Usa siempre el helper `objectId`.
44
+ - **`convert: true` es obligatorio** para query/params: las query strings llegan como string y Joi sin `convert` rechaza `Joi.number()`. Con `convert` las castea a number/date/boolean.
45
+ - **`abortEarly`**: con `false` juntas todos los errores; con `true` (default recomendado aquí) cortas en el primero — sé consistente con lo que el middleware reporta.
46
+
47
+ ## Reglas duras
48
+
49
+ 1. Toda validación en el schema, nunca inline en el controller.
50
+ 2. El schema vive en `<resource>.schema.ts`, nunca en las routes.
51
+ 3. DTO como `interface` explícita al lado del schema — mantenlos en sync (Joi no infiere tipos).
52
+ 4. Nada de `Joi.any()`: equivale a `any`, prohibido en código nuevo.
53
+ 5. Un solo validador por endpoint — no mezcles Joi + Zod (al migrar entre ambos, migra el endpoint completo).
54
+ 6. ObjectId con el helper `objectId`; query/params siempre con `convert: true`.
55
+
56
+ ## Tabla rápida
57
+
58
+ | Necesito validar | Helper |
59
+ |---|---|
60
+ | ObjectId | `objectId` (pattern `/^[a-f\d]{24}$/i`) |
61
+ | String no vacío | `Joi.string().trim().min(1)` |
62
+ | Number desde query | `Joi.number().integer().positive()` (+ `convert: true`) |
63
+ | Date | `Joi.date()` (+ `convert: true`) |
64
+ | Enum TS / literal | `Joi.string().valid(...Object.values(MyEnum))` |
65
+ | Update parcial | `schema.fork(keys, (s) => s.optional())` |
66
+ | Validación cruzada | `.custom((v, helpers) => ...)` o `Joi.object().and('a', 'b')` |
67
+
68
+ ## Antes de declarar listo
69
+
70
+ - El schema vive en `<resource>.schema.ts` y el DTO es una `interface` explícita.
71
+ - El endpoint usa `validate(schema, target)`; sin validación inline en el controller.
72
+ - Campos ObjectId con el helper `objectId`; el middleware corre con `convert: true`.
73
+ - `{{qualityGate.fast}}` en verde.
@@ -10,6 +10,8 @@ type: reference
10
10
 
11
11
  Cuando la tarea toca `domain/models` o ejecuta operaciones de Mongoose en los controllers. Mongoose 6+ sobre MongoDB. El repo no usa repository wrappers: los controllers tocan los Models directo, así que null-guards, casts de ObjectId y `.lean()` viven en cada controller method.
12
12
 
13
+ En **NestJS** (`@nestjs/mongoose`) el Model no se importa directo: se inyecta con `@InjectModel(Resource.name) private resourceModel: Model<ResourceDocument>` en el constructor del service. El resto de patrones (`.lean()`, `new Types.ObjectId`, null-guards, soft delete) aplican igual.
14
+
13
15
  ## Patrón canónico
14
16
 
15
17
  ```ts
@@ -40,7 +42,7 @@ Valida el formato en el schema (`z.string().regex(/^[a-f\d]{24}$/i, ...)`); si n
40
42
 
41
43
  - **N+1**: `populate` ejecuta queries extra. En paginate sobre datasets grandes usa `$lookup` en vez de `populate`.
42
44
  - **`.lean()`**: el resultado no tiene `.save()`, `.delete()` ni virtuals. Si necesitas mutar, no lo uses.
43
- - **Soft delete**: con `mongoose-delete`, `find` ya excluye `deleted: true`; borra con `doc.delete()` (no `findByIdAndDelete`, que es hard delete) y restaura con `doc.restore()`. Plugin sin tipos → `@ts-expect-error` puntual.
45
+ - **Soft delete**: con `mongoose-delete`, `find` ya excluye `deleted: true`; borra con `doc.delete()` (no `findByIdAndDelete`) y restaura con `doc.restore()`.
44
46
 
45
47
  ## Reglas duras
46
48
 
@@ -49,7 +51,6 @@ Valida el formato en el schema (`z.string().regex(/^[a-f\d]{24}$/i, ...)`); si n
49
51
  3. **`.lean()` cuando no necesitas mutar** — evita el overhead de documentos Mongoose.
50
52
  4. **Comparar ObjectId con `.equals()`** / `.toString()`, nunca `==`.
51
53
  5. **Respeta el soft delete del repo** — no hard delete en modelos con `mongoose-delete`.
52
- 6. **Cast explícito solo cuando hace falta** — `new Types.ObjectId(id)` para aggregations/queries complejas; nada de casts innecesarios.
53
54
 
54
55
  ## Tabla rápida
55
56
 
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: redux-toolkit
3
+ description: Patrones de Redux Toolkit en React+TS — slices, store tipado, hooks tipados, async thunks, selectores. Aplica al tocar estado global, slices o el store.
4
+ type: reference
5
+ ---
6
+
7
+ # Redux Toolkit — convenciones
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al crear o tocar un slice, el store, async thunks, o leer/escribir estado global. RTK es el estándar — nada de `createStore` pelado, action types a mano, ni `connect`. Estado de servidor (fetch/cache) NO va aquí: eso es TanStack Query. Redux es para estado de cliente compartido (sesión, UI cross-página, carrito).
12
+
13
+ ## El patrón
14
+
15
+ ```ts
16
+ const slice = createSlice({
17
+ name: 'session',
18
+ initialState,
19
+ reducers: {
20
+ setActive(state, action: PayloadAction<Session>) {
21
+ state.active = action.payload; // Immer: "mutas" un draft, no el real
22
+ },
23
+ },
24
+ extraReducers: (b) => {
25
+ b.addCase(loadSession.fulfilled, (s, a) => { s.active = a.payload; });
26
+ },
27
+ });
28
+ export const { setActive } = slice.actions;
29
+ ```
30
+
31
+ Store + hooks tipados una sola vez, y se usan en toda la app:
32
+
33
+ ```ts
34
+ export const useAppDispatch: () => AppDispatch = useDispatch;
35
+ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
36
+ ```
37
+
38
+ ## Gotchas que muerden
39
+
40
+ - **Immer solo dentro de `createSlice`.** Ahí "mutas" el draft; fuera de un reducer, mutar el state es un bug. No retornes Y mutes en el mismo reducer.
41
+ - **Selectores memoizados** con `createSelector` cuando derivan/transforman — un selector que crea un array/objeto nuevo en cada llamada re-renderiza siempre.
42
+ - **`useSelector` devuelve la referencia**: selecciona lo mínimo, no el slice entero.
43
+ - **Async**: `createAsyncThunk` para casos simples; si es data de API que cacheas/invalidas, evalúa RTK Query en vez de thunks + slice manual.
44
+ - **No-serializables** (Date, Map, funciones) fuera del store; rompen devtools y persistencia.
45
+
46
+ ## Reglas duras
47
+
48
+ 1. Estado global solo vía slices de RTK; nada de Context improvisado para lo mismo.
49
+ 2. Hooks `useAppDispatch`/`useAppSelector` tipados, nunca los crudos sin tipo.
50
+ 3. Selecciona lo mínimo y memoiza los derivados con `createSelector`.
51
+ 4. Estado de servidor no vive en Redux — eso es cache de queries.
52
+ 5. Solo valores serializables en el store.
53
+
54
+ ## Antes de declarar listo
55
+
56
+ - El slice nuevo expone acciones tipadas y se consume con los hooks tipados.
57
+ - Los selectores derivados están memoizados; los componentes seleccionan lo mínimo.
58
+ - Nada de data de API duplicada en el store si ya hay capa de queries.
59
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: socketio
3
+ description: Patrones de Socket.IO en un servicio Node — namespaces, rooms, auth en el handshake, eventos tipados, cleanup. Aplica al tocar realtime, gateways o handlers de socket.
4
+ type: reference
5
+ ---
6
+
7
+ # Socket.IO — convenciones del servicio
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al agregar o tocar realtime: un namespace, un evento, autenticación de conexión, o broadcast a un room. Socket.IO sobre el HTTP server de Express. La regla base: el handler de socket es una capa de transporte, no de negocio — delega al mismo service/controller que usan las rutas HTTP.
12
+
13
+ ## El patrón
14
+
15
+ ```ts
16
+ io.of('/sessions').use(authSocket).on('connection', (socket) => {
17
+ socket.join(`session:${socket.data.sessionId}`);
18
+
19
+ socket.on('message:send', async (dto, ack) => {
20
+ try {
21
+ const saved = await messageService.create(socket.data.userId, dto);
22
+ io.to(`session:${dto.sessionId}`).emit('message:new', saved);
23
+ ack?.({ ok: true, id: saved._id });
24
+ } catch (err) {
25
+ ack?.({ ok: false, error: toClientError(err) });
26
+ }
27
+ });
28
+
29
+ socket.on('disconnect', () => { /* cleanup timers/subscriptions */ });
30
+ });
31
+ ```
32
+
33
+ `authSocket` valida el token en `socket.handshake.auth.token` y rellena `socket.data` (userId/sessionId). Nunca confíes en un id que venga en el payload del evento sin cruzarlo contra `socket.data`.
34
+
35
+ ## Gotchas que muerden
36
+
37
+ - **Rooms, no broadcast global.** `io.emit` manda a todos los conectados; usa `io.to(room)` / `socket.to(room)` para no filtrar datos entre sesiones/tenants.
38
+ - **`socket.emit` vs `io.to(...).emit`.** `socket.emit` responde solo al emisor; para incluirte y al resto del room usa `io.to(room)`, para excluirte usa `socket.to(room)`.
39
+ - **Listeners colgados.** Toda suscripción/intervalo creado en `connection` se limpia en `disconnect`, o se filtra memoria.
40
+ - **Errores.** Un throw dentro de un handler no llega al cliente: reporta vía el callback `ack` o un evento `error:*`, nunca dejes la promesa sin catch.
41
+ - **Auth en el handshake**, no por evento — rechaza en el middleware `.use()` antes de `connection`.
42
+
43
+ ## Reglas duras
44
+
45
+ 1. El handler delega al service; nada de queries ni lógica de negocio inline.
46
+ 2. Identidad desde `socket.data` (poblado en auth), nunca desde el payload.
47
+ 3. Emite a un room específico; `io.emit` global solo para health/system.
48
+ 4. Cada `on(...)` con efectos secundarios tiene su cleanup en `disconnect`.
49
+ 5. Errores al cliente vía `ack`/evento `error`, con el mismo `ApiError` mapeado que HTTP.
50
+
51
+ ## Antes de declarar listo
52
+
53
+ - Los eventos nuevos validan su input igual que un endpoint HTTP.
54
+ - Ningún `io.emit` global salvo señales de sistema; el resto va por room.
55
+ - Auth resuelta en el middleware del namespace, no dentro de los handlers.
56
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: tanstack-query
3
+ description: Patrones de TanStack Query (React Query) — query keys, mutations, invalidación, staleTime. Aplica al tocar fetching, cache de servidor o mutaciones de datos remotos.
4
+ type: reference
5
+ ---
6
+
7
+ # TanStack Query — convenciones
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al leer/escribir datos del servidor: un `useQuery`, un `useMutation`, invalidar cache, o paginar. TanStack Query es la fuente de verdad del **estado de servidor** (fetch + cache + revalidación). No lo uses para estado de cliente puro (eso es useState/Redux), ni dupliques su data en otro store.
12
+
13
+ ## El patrón
14
+
15
+ Query keys estructuradas y centralizadas para invalidar sin strings sueltos:
16
+
17
+ ```ts
18
+ const sessionKeys = {
19
+ all: ['sessions'] as const,
20
+ detail: (id: string) => [...sessionKeys.all, id] as const,
21
+ };
22
+
23
+ const { data, isPending, error } = useQuery({
24
+ queryKey: sessionKeys.detail(id),
25
+ queryFn: () => api.getSession(id),
26
+ staleTime: 30_000,
27
+ enabled: Boolean(id),
28
+ });
29
+
30
+ const mutation = useMutation({
31
+ mutationFn: api.updateSession,
32
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: sessionKeys.all }),
33
+ });
34
+ ```
35
+
36
+ ## Gotchas que muerden
37
+
38
+ - **Query keys como datos, no strings.** Una factory (`sessionKeys`) evita typos y permite invalidar por prefijo (`sessionKeys.all` invalida todos los detalles).
39
+ - **`staleTime` vs `gcTime`.** `staleTime` decide cuándo refetchea; con `0` (default) refetchea agresivo. Súbelo para data estable y evita parpadeos/llamadas extra.
40
+ - **No espejes la data en useState/Redux.** Lee de `data` directo; copiarla a otro estado crea dos verdades que se desincronizan.
41
+ - **`enabled`** para queries dependientes — no dispares con el id aún `undefined`.
42
+ - **Invalidación tras mutar**, no edición manual del cache, salvo update optimista deliberado con rollback en `onError`.
43
+ - **`isPending` vs `isFetching`**: `isPending` es la primera carga sin data; `isFetching` es cualquier fetch en curso (incluye revalidación).
44
+
45
+ ## Reglas duras
46
+
47
+ 1. Estado de servidor vive en Query; no se copia a otro store.
48
+ 2. Query keys desde una factory tipada, nunca arrays literales dispersos.
49
+ 3. Tras una mutation, invalida las keys afectadas.
50
+ 4. `enabled` en queries dependientes; nada de queries con params inválidos.
51
+ 5. `staleTime` explícito cuando la data no cambia cada segundo.
52
+
53
+ ## Antes de declarar listo
54
+
55
+ - Las keys nuevas salen de la factory y se invalidan tras mutar.
56
+ - Ningún dato de query duplicado en useState/Redux.
57
+ - Las queries dependientes usan `enabled`.
58
+ - `{{qualityGate.fast}}` en verde.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Antes de cerrar la sesión:
4
4
 
5
- 1. **Quality gate**: corre `{{qualityGate.full}}` y confirma que pasa (o documenta deuda en `progress/current.md`).
5
+ 1. **Quality gate**: {{qualityGate.full}} confirma que pasa (o documenta deuda en `progress/current.md`).
6
6
  2. **History**: agrega entrada en `progress/history.md` con `## YYYY-MM-DD HH:MM <agente> — <resumen>` + cambios + estado del gate.
7
7
  3. **Vaciar current**: deja `progress/current.md` en estado `idle` o con el siguiente paso explícito.
8
8
  4. **Sin temporales**: borra scratch files, no dejes `console.log`, `debugger`, ni código comentado.
@@ -1,4 +1,4 @@
1
1
  ## Idioma y rol
2
2
 
3
- - Código/JSDoc: inglés. Chat: español MX.
3
+ - Código y comentarios (JSDoc/docstrings): inglés. Chat: español MX.
4
4
  - Rol Tech Lead Senior. Antes de codear: ¿lo más simple? ¿legible en 6 meses? ¿mantiene patrón existente? Simplicidad > cleverness.
@@ -4,5 +4,6 @@ Read-only por default. Antes de mutar datos, esquema o infraestructura (DB, stor
4
4
 
5
5
  - **DB / queries**: por default solo lectura (`SELECT`, `EXPLAIN`, flags tipo `onlyRead`). `INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE` requieren que el usuario lo pida de forma explícita.
6
6
  - **Comandos de shell**: inspeccionar es libre (`ls`, `cat`, `git status/diff/log`). Los destructivos (`rm -rf`, `git reset --hard`, force-push, `chmod -R`) los manda el harness a `ask`/`deny` y un hook los bloquea — no intentes evadir esa capa.
7
+ - **Búsqueda de código**: usa las tools nativas `Glob` (archivos por nombre/patrón) y `Grep` (contenido). Son read-only, más rápidas (ripgrep por debajo) y ya saltan `node_modules`/`.git`, así que no piden permiso. Reserva `find`/`grep` por shell para lo que las tools no cubren — búsqueda por metadata del FS (`-size`, `-mtime`, permisos) — y úsalo solo cuando sea críticamente necesario. `find` no está pre-aprobado a propósito: con `-exec`/`-delete` no es read-only puro, así que pedir permiso ahí es la red de seguridad correcta, no un estorbo.
7
8
  - **Si una mutación destructiva es legítima y necesaria**: explica qué hace y por qué, y deja que el usuario la confirme o la corra. Nunca la disfraces con variables, subshells o `--no-verify` para saltarte el gate.
8
9
  - **Datos sensibles**: no vuelques secretos, PII ni dumps completos a logs, chat o archivos del repo.
@@ -0,0 +1,18 @@
1
+ ## Stack — Background worker (jobs + queues)
2
+
3
+ Proceso de fondo en Node/TS cuyo trabajo es **procesar jobs y mensajes**, no servir HTTP. El flujo típico: un scheduler (`agenda` / `bullmq` / `node-cron`) dispara jobs en el tiempo, y/o un consumidor de cola (`amqplib` / `bullmq` / `kafkajs`) reacciona a mensajes. Cada handler hace su trabajo (mandar email, push, recalcular, sincronizar) y reporta éxito/fallo a la infraestructura de jobs.
4
+
5
+ Aunque el repo tenga `express` en deps, **no expone endpoints de negocio** — a lo sumo un `/health` para el orquestador. Si te piden agregar una "ruta", confirma: casi siempre es un job o un consumer nuevo, no un endpoint.
6
+
7
+ Reglas de oro:
8
+ - **Idempotencia**: un job/mensaje puede entregarse más de una vez. Todo handler debe ser seguro de re-ejecutar (claves de deduplicación, upserts, chequear estado antes de actuar).
9
+ - **Graceful shutdown**: en `SIGTERM`/`SIGINT`, deja de tomar trabajo nuevo, espera a que los jobs en vuelo terminen (con timeout) y cierra conexiones (DB, broker) antes de salir. Nunca mates un job a la mitad sin re-encolar.
10
+ - **Errores explícitos**: un fallo se reintenta con backoff o va a una dead-letter; nunca se traga en silencio. El logging va por el `Logger` estructurado, nunca `console.log`.
11
+ - **Nada de `process.env`** fuera del módulo de config.
12
+
13
+ Aplica las skills según la capa que toques:
14
+ - `worker-lifecycle` — bootstrap, graceful shutdown, healthcheck, no servir HTTP de negocio.
15
+ - `job-scheduling` — definir/agendar jobs (agenda/bullmq), idempotencia, reintentos con backoff.
16
+ - `queue-consumers` — consumir mensajes (amqplib/bullmq), `ack`/`nack`, dead-letter, backpressure.
17
+
18
+ El logging y el flujo de tickets/PR los cubre el harness base (agentes `leader`, `implementer`, `reviewer`, `commit-pr-pilot` y las skills core).
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: job-scheduling
3
+ description: Definir y agendar jobs en un worker (agenda / bullmq) — idempotencia, reintentos con backoff, concurrencia. Aplica al crear o tocar un job programado o recurrente.
4
+ type: reference
5
+ ---
6
+
7
+ # job-scheduling — jobs idempotentes y reintentables
8
+
9
+ Un job define **qué** hacer; el scheduler decide **cuándo** y **cuántas veces**. Como un job puede correr más de una vez (reintento, doble disparo), el handler debe ser idempotente.
10
+
11
+ ## Cuándo usar este skill
12
+
13
+ Al definir un job nuevo, agendar uno recurrente, o ajustar reintentos/concurrencia.
14
+
15
+ ## El patrón (agenda)
16
+
17
+ Un archivo por job (`<name>.job.ts`) que registra el handler; el scheduling vive aparte del handler.
18
+
19
+ ```ts
20
+ export function defineSyncJob(agenda: Agenda) {
21
+ agenda.define('sync-user', { concurrency: 5, lockLifetime: 60_000 }, async (job) => {
22
+ const { userId } = job.attrs.data as { userId: string };
23
+ // idempotente: chequea estado antes de actuar
24
+ if (await alreadySynced(userId, job.attrs.lastRunAt)) return;
25
+ await syncUser(userId);
26
+ });
27
+ }
28
+
29
+ // scheduling, separado del handler:
30
+ await agenda.every('0 * * * *', 'sync-user', { userId }); // recurrente
31
+ await agenda.schedule('in 5 minutes', 'sync-user', { userId }); // one-off
32
+ ```
33
+
34
+ bullmq es equivalente: `new Worker(name, handler, { concurrency })` + `queue.add(name, data, { repeat, attempts, backoff })`.
35
+
36
+ ## Gotchas que muerden
37
+
38
+ - **Doble disparo**: dos instancias del worker pueden tomar el mismo job. agenda usa `lockLifetime`; bullmq usa locks por job. Aun así, **el handler debe ser idempotente** — no confíes solo en el lock.
39
+ - **Reintentos sin backoff** martillan un servicio caído. Configura `attempts` + `backoff` exponencial.
40
+ - **`lockLifetime` corto** + job largo → el lock expira y otro worker lo retoma en paralelo. Ajústalo por encima de la duración real del job.
41
+
42
+ ## Reglas duras
43
+
44
+ 1. Un job por archivo `<name>.job.ts`; handler separado del scheduling.
45
+ 2. Handler **idempotente**: chequea estado antes de mutar; usa upserts/claves de dedup.
46
+ 3. Reintentos con backoff exponencial y un tope (`attempts`); sin reintento infinito.
47
+ 4. `concurrency` y `lockLifetime` explícitos y coherentes con la duración del job.
48
+ 5. Nada de trabajo no idempotente que dependa de "correr exactamente una vez".
49
+
50
+ ## Antes de declarar listo
51
+
52
+ - El handler es seguro de re-ejecutar (probado corriéndolo dos veces).
53
+ - Reintentos con backoff y tope configurados.
54
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: queue-consumers
3
+ description: Consumir mensajes de una cola en un worker (amqplib / bullmq) — ack/nack, dead-letter, prefetch/backpressure, idempotencia. Aplica al crear o tocar un consumidor de cola.
4
+ type: reference
5
+ ---
6
+
7
+ # queue-consumers — consumir sin perder ni duplicar
8
+
9
+ Un consumer reacciona a mensajes. La regla central: **un mensaje no se confirma (`ack`) hasta que se procesó con éxito**; si falla, se re-encola o va a dead-letter — nunca se pierde en silencio.
10
+
11
+ ## Cuándo usar este skill
12
+
13
+ Al crear un consumer, manejar fallos de procesamiento, o ajustar prefetch / dead-letter.
14
+
15
+ ## El patrón (amqplib)
16
+
17
+ ```ts
18
+ await channel.prefetch(10); // backpressure: máx 10 sin ack a la vez
19
+ await channel.consume(queue, async (msg) => {
20
+ if (!msg) return;
21
+ try {
22
+ const payload = JSON.parse(msg.content.toString());
23
+ if (await alreadyProcessed(payload.id)) { channel.ack(msg); return; } // idempotente
24
+ await handle(payload);
25
+ channel.ack(msg);
26
+ } catch (err) {
27
+ logger.error({ err }, 'consume failed');
28
+ // requeue una vez; si ya fue redelivered, mándalo a la DLQ (no requeue infinito)
29
+ channel.nack(msg, false, !msg.fields.redelivered);
30
+ }
31
+ });
32
+ ```
33
+
34
+ bullmq: lanzar dentro del `Worker` handler re-encola según `attempts`/`backoff`; al agotarse, el job queda `failed` (tu DLQ lógica).
35
+
36
+ ## Gotchas que muerden
37
+
38
+ - **`nack` con `requeue: true` siempre** → loop infinito si el mensaje es venenoso. Requeue una vez (chequea `redelivered`), luego dead-letter.
39
+ - **Sin `prefetch`** el consumer traga toda la cola en memoria. Fija un prefetch acorde a la duración del handler.
40
+ - **`ack` antes de procesar** = pérdida de mensajes si el handler crashea. Confirma **después** del éxito.
41
+ - **Mensajes duplicados** son normales (redelivery). El handler debe ser idempotente.
42
+
43
+ ## Reglas duras
44
+
45
+ 1. `ack` solo tras éxito; en fallo, `nack`/requeue acotado o dead-letter.
46
+ 2. Nunca requeue infinito de un mensaje venenoso — DLQ tras el primer redelivery.
47
+ 3. `prefetch` explícito para backpressure.
48
+ 4. Handler **idempotente**: chequea dedup antes de actuar.
49
+ 5. Errores logueados (estructurado), nunca tragados en silencio.
50
+
51
+ ## Antes de declarar listo
52
+
53
+ - Un mensaje que falla no se pierde ni hace loop infinito (va a DLQ).
54
+ - `prefetch` configurado; el handler es idempotente.
55
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: worker-lifecycle
3
+ description: Ciclo de vida de un worker de fondo en Node/TS — bootstrap, graceful shutdown, healthcheck mínimo, sin servir HTTP de negocio. Aplica al tocar el arranque/apagado del proceso o la conexión a DB/broker.
4
+ type: reference
5
+ ---
6
+
7
+ # worker-lifecycle — arrancar y apagar limpio
8
+
9
+ Un worker no es un HTTP server: arranca conexiones, registra schedulers/consumers, y debe **apagar limpio** sin matar trabajo en vuelo. Su `main` orquesta el bootstrap y un único shutdown idempotente.
10
+
11
+ ## Cuándo usar este skill
12
+
13
+ Al tocar `index.ts`/`main.ts`, el arranque del scheduler/consumer, la conexión a Mongo/broker, o el manejo de señales.
14
+
15
+ ## El patrón
16
+
17
+ ```ts
18
+ async function main() {
19
+ const db = await connectMongo(config.mongoUri);
20
+ const broker = await connectBroker(config.amqpUrl);
21
+ const scheduler = startScheduler({ db }); // job-scheduling
22
+ const consumer = startConsumer({ broker }); // queue-consumers
23
+
24
+ const shutdown = once(async (signal: string) => {
25
+ logger.info({ signal }, 'shutting down');
26
+ await consumer.stop(); // deja de tomar mensajes nuevos
27
+ await scheduler.stop(); // deja de disparar jobs
28
+ await drainInflight(15_000); // espera lo en vuelo, con timeout
29
+ await broker.close();
30
+ await db.close();
31
+ process.exit(0);
32
+ });
33
+
34
+ for (const sig of ['SIGTERM', 'SIGINT'] as const) process.on(sig, () => shutdown(sig));
35
+ }
36
+ main().catch((err) => { logger.error({ err }, 'fatal on boot'); process.exit(1); });
37
+ ```
38
+
39
+ `once` garantiza que dos señales seguidas no disparen dos shutdowns. El orden importa: **primero dejas de aceptar trabajo**, luego drenas lo en vuelo, luego cierras conexiones.
40
+
41
+ ## Healthcheck (si el orquestador lo exige)
42
+
43
+ Un solo endpoint `/health` con un `http.createServer` mínimo está bien — **no es** una API. Devuelve `200` si las conexiones (DB, broker) están vivas. Nada de rutas de negocio aquí.
44
+
45
+ ## Reglas duras
46
+
47
+ 1. Un único punto de shutdown, idempotente (`once`), escuchando `SIGTERM` y `SIGINT`.
48
+ 2. Dejar de aceptar trabajo **antes** de drenar; drenar con timeout; cerrar conexiones al final.
49
+ 3. Nunca `process.exit` a mitad de un job sin re-encolarlo o dejarlo `nack`-eado.
50
+ 4. Sin rutas HTTP de negocio. `/health` es el único endpoint permitido.
51
+ 5. Errores de arranque → log estructurado + `exit(1)`; no arranques a medias.
52
+
53
+ ## Antes de declarar listo
54
+
55
+ - `SIGTERM` apaga limpio: sin jobs muertos a la mitad, conexiones cerradas.
56
+ - El proceso no expone endpoints de negocio.
57
+ - `{{qualityGate.fast}}` en verde.