Fase 2: 04_sanitize.py - scrubbing de secretos de skills/agentes/plans (10 secretos unicos, gate en verde)

This commit is contained in:
2026-07-29 00:35:59 +00:00
parent 21bc219f31
commit 837f91060f
79 changed files with 10153 additions and 0 deletions
@@ -0,0 +1,125 @@
# Skill global: cargar contexto de Docmost al iniciar conversación
## Contexto
El usuario quiere que, al iniciar cualquier conversación de Claude Code, se cargue automáticamente el
contexto del proyecto actual desde Docmost (MCP `mcp__docmost__*`): listar los spaces, identificar cuál
corresponde al proyecto en el que se está trabajando, y leer el contenido completo de todas sus páginas.
Investigación realizada (en modo solo-lectura):
- No existe ningún CLAUDE.md global ni hook `SessionStart` hoy en `~/.claude/settings.json` (solo hay un
hook `Notification` para email vía Gmail).
- Las **skills se activan por coincidencia de descripción**, no automáticamente al iniciar sesión. Para que
algo corra *siempre* al inicio, el único mecanismo real es un **hook `SessionStart`**, cuyo `stdout` se
inyecta como contexto adicional (esto es lo que documenta `hooks-guide.md` de Claude Code, ej. "Re-inject
context after compaction"). El hook (bash) no puede llamar herramientas MCP directamente — eso solo lo
puede hacer el modelo. Por eso el diseño es: **hook (bash) → inyecta instrucción → skill (markdown) →
modelo ejecuta las llamadas MCP reales**.
- Ya existe en `background-orchestrator/SKILL.md` una lógica de detección de proyecto reutilizable
(`package.json``pyproject.toml` → basename del repo git), que se reutiliza aquí tal cual.
- Verifiqué en vivo el MCP de Docmost: `list_spaces` devuelve `{id, name, slug, description, ...}`;
`list_pages({spaceId})` devuelve el árbol de páginas (`id, title, parentPageId`) sin contenido;
`get_page({pageId})` sí devuelve el contenido completo (`content`, markdown). Los nombres de space no
siempre calzan literal con el nombre del repo (ej. space "Create React SSR App" vs. un repo en
kebab-case), así que el match debe ser normalizado (minúsculas, sin espacios/guiones), no exacto.
Algunos spaces ya tienen 30+ páginas con contenido extenso.
Decisiones confirmadas con el usuario:
1. **Leer todo siempre** — al detectar el space, leer el contenido completo de todas sus páginas (sin
recortar ni resumir antes de cargar).
2. **Si no hay match** — preguntar al usuario (mostrar los spaces disponibles y dejar que elija uno o
indique que no corresponde ninguno).
3. **Alcance** — el hook solo actúa si el cwd está dentro de un repo git; fuera de un repo no intenta nada.
## Diseño
### 1. Script del hook — `~/.claude/hooks/docmost-session-start.sh` (nuevo, `chmod +x`)
Bash puro, sin llamadas MCP (no puede hacerlas). Solo detecta el proyecto y, si aplica, imprime a stdout
una instrucción para que el modelo invoque la skill. Si no está en un repo git, no imprime nada (`exit 0`
silencioso → no se inyecta contexto ni se gasta turno en esto).
```bash
#!/bin/bash
# Solo actuar dentro de un repo git (decisión confirmada con el usuario)
git rev-parse --show-toplevel >/dev/null 2>&1 || exit 0
PROJECT_NAME=$(jq -r '.name // empty' package.json 2>/dev/null)
[ -z "$PROJECT_NAME" ] && PROJECT_NAME=$(grep -oP '^\s*name\s*=\s*"\K[^"]+' pyproject.toml 2>/dev/null | head -1)
[ -z "$PROJECT_NAME" ] && PROJECT_NAME=$(basename "$(git rev-parse --show-toplevel)")
cat <<EOF
[docmost-context] Nueva conversación detectada en el proyecto git "$PROJECT_NAME" ($(pwd)).
Antes de responder al primer mensaje del usuario, invoca la skill "docmost-context" (Skill tool,
skill: docmost-context, args: "$PROJECT_NAME") para cargar el contexto del proyecto desde Docmost.
Hazlo una sola vez al inicio de esta conversación; no la vuelvas a invocar en turnos posteriores salvo
que el usuario lo pida explícitamente.
EOF
```
### 2. Registrar el hook en `~/.claude/settings.json` (editar)
Añadir la clave `SessionStart` dentro de `hooks` (se conserva el `Notification` existente). Se registran
tres matchers por separado (`startup`, `resume`, `clear`) — deliberadamente **sin** `compact`, para no
releer 30+ páginas completas cada vez que el contexto se compacta a mitad de conversación:
```json
"SessionStart": [
{ "matcher": "startup", "hooks": [{ "type": "command", "command": "/home/aleleba/.claude/hooks/docmost-session-start.sh" }] },
{ "matcher": "resume", "hooks": [{ "type": "command", "command": "/home/aleleba/.claude/hooks/docmost-session-start.sh" }] },
{ "matcher": "clear", "hooks": [{ "type": "command", "command": "/home/aleleba/.claude/hooks/docmost-session-start.sh" }] }
]
```
También añadir a `permissions.allow`: `"mcp__docmost__list_spaces"` (los demás —`list_pages`, `get_page`,
`search`— ya están permitidos desde `background-orchestrator`).
### 3. Skill global — `~/.claude/skills/docmost-context/SKILL.md` (nuevo)
```yaml
---
name: docmost-context
description: >-
Carga el contexto del proyecto actual desde Docmost (MCP) al inicio de una conversación: lista los
spaces, identifica cuál corresponde al proyecto del directorio actual, y lee el contenido completo
de todas sus páginas. Se activa automáticamente vía el hook SessionStart (ver
~/.claude/hooks/docmost-session-start.sh) — no requiere que el usuario la invoque a mano, aunque
también responde a "carga el contexto de Docmost", "lee el proyecto en Docmost", "/docmost-context".
effort: medium
argument-hint: "[nombre del proyecto, si ya se conoce]"
---
```
Cuerpo (workflow):
1. **Determinar el nombre del proyecto**: usar el argumento recibido si viene; si no, repetir la misma
detección de `background-orchestrator` (`package.json``pyproject.toml` → basename del repo git).
2. **Listar spaces**: `mcp__docmost__list_spaces`.
3. **Buscar coincidencia** comparando el nombre del proyecto contra `name` y `slug` de cada space,
normalizando (minúsculas, sin espacios/guiones/guiones bajos) — match exacto normalizado, no
substring débil.
- **Una sola coincidencia** → usarla directamente, sin confirmar con el usuario.
- **Cero coincidencias** → usar `AskUserQuestion` mostrando la lista de spaces disponibles, dejando
elegir uno existente o responder que ninguno corresponde (en ese caso, no cargar nada y continuar
la conversación normalmente).
- **Varias coincidencias ambiguas** → preguntar igual, mostrando solo los candidatos ambiguos.
4. **Leer el space completo**: `mcp__docmost__list_pages({spaceId})` para obtener todas las páginas, y
luego `mcp__docmost__get_page({pageId})` para **cada una** (contenido completo, sin omitir ninguna).
5. **No repetir el dump al usuario**: una vez cargado el contenido al contexto, responder con un mensaje
breve (1-2 líneas) indicando qué space y cuántas páginas se cargaron — no transcribir el contenido de
las páginas en el chat.
6. Ejecutar esto **una sola vez** por conversación (al primer turno, disparado por el hook); no repetir
en turnos siguientes salvo pedido explícito del usuario.
## Verificación
1. `chmod +x ~/.claude/hooks/docmost-session-start.sh` y ejecutarlo manualmente dentro de un repo git y
fuera de uno, confirmando que imprime la instrucción solo en el primer caso.
2. Iniciar una conversación nueva (`startup`) dentro de un repo con space conocido (ej. `Ro-ut`) y
confirmar que el modelo invoca la skill sin que el usuario lo pida, y que reporta el space/páginas
cargadas en 1-2 líneas.
3. Probar dentro de un repo **sin** space coincidente y confirmar que se dispara `AskUserQuestion` con la
lista de spaces.
4. Si en la prueba real el `stdout` plano del hook no llega como contexto, usar como fallback el formato
JSON documentado: `{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "..."}}`.
5. Confirmar que `/clear` también dispara la carga, y que `compact` (compactación automática) **no** la
repite.