Imported from carlos18bp/tuhuella_project (
.agents/skills/vuln-audit/SKILL.md). Install upstream withnpx skills add carlos18bp/tuhuella_project --skill vuln-audit. Copyright stays with the author.
Cuándo usar cuál (familia de auditoría)
| Skill | Úsala cuando | Cadencia típica |
|---|---|---|
$full-audit |
Veredicto integral 🟢/🟡/🔴 del VPS o del fleet (--all): configs, drift, envs, timers, health, email — 12 fases automatizadas, ~4 min |
Post-cambio grande, post-incidente, trimestral |
$server-diagnostic |
Informe profundo por las 15 buenas prácticas con score y recomendaciones por proyecto — más narrativo y granular que full-audit | Semanal automático (cron) / a demanda |
$vuln-audit |
Dependencias y CVEs de UN proyecto (pip + npm): default arma el plan sin tocar nada; --apply aplica patch+minor en batch; --upgrade moderniza todo (majors incluidos) commit a commit con CI verde por commit |
Por proyecto: mensual o ante CVE; --upgrade trimestral o al planificar una modernización |
Se orquestan en UNA sola dirección: $deploy-and-check (manual-only) corre el motor de $server-diagnostic al cierre de cada deploy (modo consumidor, read-only: sin email, sin histórico, .md aparte) y sugiere $vuln-audit cuando su sonda de dependencias lo amerita; $full-audit NO corre a las otras dos y ninguna skill auto-invoca $deploy-and-check.
vuln-audit — Vulnerability & Dependency Audit (multi-stack)
Goal
Replicar de forma automática el flujo manual de auditoría que vive en audit-report.md de los proyectos del repo, en tres niveles:
- Default (report-first, read-only): escanear vulns + outdated y construir el plan de bumps como tabla (paquete · versión actual → propuesta · severidad · tipo patch/minor/major · unidad · riesgo), sin escribir NADA — ni archivos, ni installs sobre el proyecto, ni commits.
--apply(o selección en el menú post-reporte): aplicar en batch los bumps patch+minor del plan dentro del major actual respetando pins existentes, verificar con checks mínimos y dejar hasta 3 commits separados en la rama de trabajo (frontend deps → backend deps → reporte), con push + PR al final.--upgrade(modernización secuencial): llevar TODAS las dependencias directas de las superficies pedidas a la última versión que permiten los constraints, en UN solo PR, con un commit por unidad semver (batch patch → batch minor → cada major en su propio commit), pusheando cada commit solo y avanzando únicamente con su CI verde. Un major que no queda verde con esfuerzo acotado se revierte (git revert, nunca force push) y queda diferido enaudit-report.mdcon la causa. El CI del PR es la verificación; lo local es smoke.
Inputs
$ARGUMENTS(opcional). Valores aceptados (combinables):- vacío → auditar
backend/yfrontend/. backend→ solo Python.frontend→ solo npm.--apply→ además de auditar, aplicar en batch los bumps patch+minor del plan (fase de escritura). Sin él ni--upgrade, la corrida es solo auditoría.--upgrade→ modernización secuencial (ver## Modo --upgrade). Excluyente con--apply: si vienen los dos, abortar con «--applyy--upgradeson excluyentes: elegí uno». Combinable conbackend/frontend: restringe el loop a esa superficie; worktree, rama y PR son los mismos (deps-upgrade), así que una corrida posterior con la otra superficie los reutiliza y el PR termina cubriendo ambas.
- vacío → auditar
- Cualquier otro valor: abortar con mensaje pidiendo uno de los aceptados.
Constraints (no negociables)
- Default read-only. Sin
--apply/--upgrade, la skill NO modifica nada: nipackage.json/package-lock.json, nirequirements.txt, niaudit-report.md, ni commits, ni ramas. Solo snapshots a/tmpy el plan en la respuesta. - Branching (solo modos mutantes): sigue el protocolo por sesión: worktree propio bajo
~/webapps/.wt/<repo>/— slugvuln-auditpara--apply(ramachore/<DDMMYYYY>-vuln-audit) y slugdeps-upgradepara--upgrade(ramachore/<DDMMYYYY>-deps-upgrade) — cortado de la BASE que resuelve la coordenada (resolved_branchsi hay release activa víapr_state=single; si no, la default del repo — nunca la default a secas ignorando la coordenada) víasession-worktree.sh create chore <slug>; nunca checkout de ramas ajenas ni rama nueva en el clon principal. Si la sesión YA tiene su worktree/rama de un turno anterior, se reutiliza. Detalle operativo en la Fase 0. - Push + PR al primer push (tmpl §9, con
Sesión:/Intención:en el body); la skill PARA con el PR abierto — el merge es del operador/$merge-queue, nunca de esta skill. En--applyel push va al final; en--upgrade, con el primer commit. - Working tree debe estar limpio antes de aplicar (
git statussin cambios). Si no lo está, abortar el modo mutante; la auditoría read-only puede correr igual (no toca el tree). - Alcance por modo.
--apply: solo patch + minor dentro del major actual.--upgrade: los majors entran únicamente como unidad propia (un paquete o grupo lockstep por commit), jamás dentro de un batch. En ambos modos0.x → 0.y(y0.x → 1.x) es major, y Django cuenta cada feature release como major (6.0 → 6.1,6.1 → 6.2: cambian el soporte de bases de datos y quitan APIs aunque no cambie el primer número; sóloX.Y.Z → X.Y.Wes patch) —--applyla salta,--upgradela trata como unidad framework con el gate de la base de producción. Nuncanpm audit fix --force. - Respetar pins y constraints documentados:
requirements.txtcon<X.Yo>=A,<B, los comentarios inline del propiorequirements.txt, los constraints deCLAUDE.md/AGENTS.mdy elnode_versionde producción enprojects.yml. Un bump que los viole se salta y se reporta; nunca se fuerza ni se edita el constraint. - Venv aislado en el worktree (modos mutantes). El venv del clon principal es el del servicio en producción y
session-worktree.shno lo enlaza a propósito. Toda instalación de Python en un worktree va abackend/.venvcreado DENTRO del worktree (python3 -m venv backend/.venv, gitignored); jamássourcede un venv fuera del worktree, jamáspip installcon el pip del clon principal. El modo read-only usa el venv del proyecto sólo para leer (lo único que instala ahí espip-auditsi falta — contrato conscripts/lib/deps-probes.sh). - Sin base de datos desde el worktree.
backend/.envdel worktree es un enlace al.envde PRODUCCIÓN (en mimittos apunta al MySQL real). Verificación local backend =pip check+python manage.py check+pytest --collect-only -q, nada más; un slice de tests corre SÓLO sibackend/pytest.inideclara unsettings_test(sqlite) o si los settings del proyecto leen el motor del entorno (linaje base_feature:DJANGO_DB_ENGINE) y el comando lo fuerza a sqlite (DJANGO_DB_ENGINE=django.db.backends.sqlite3). Nuncamanage.py migrate, nunca tests nimanage.pyconDJANGO_ENV=production/settings de producción desde el worktree — única excepción, sólo en--upgradey sólo para un major de Django omysqlclient:manage.py check --database defaultcon el selector de producción, que abre la conexión de sólo lectura y corre los checks del backend MySQL que el CI (sqlite) no puede ver; jamásmigrate,shell,dbshellni tests con esos settings. La suite completa la corre el CI. - Nunca correr la suite completa de tests en local (regla "never run the full suite" de los
CLAUDE.md): en--applysólopytest --collect-only+ el slice sqlite si existe; en--upgradela suite la corre el CI de cada commit y lo local es smoke de ≤3 comandos por unidad. 0.x → 0.yes major también paranpm-check-updates: la corrida--target minorlleva--rejectcon la lista literal de los paquetes directos cuyocurrentes0.x; un0.x → 0.ysólo entra como unidad major en--upgrade.- Ritmo estricto (
--upgrade). Un commit por unidad, un push por commit; nunca se pushea mientras el CI del push anterior está en vuelo (los workflows tienencancel-in-progress); nada avanza sinbucket=passen todos los checks. Rojo ⇒ corrección acotada con commit NUEVO ogit revert; jamás--amend/force push después de un push (política de capa 1: denegado en toda forma). Topes: ≤2 iteraciones de fix por major, ≤2 pin-backs por batch; después, revert (major) o ⏸️ (batch). - Node de producción para instalar y compilar. Todo
npm ci/npm install/npm run buildde un worktree corre con el major de node con el que el artefacto va a correr en PRODUCCIÓN: elnode_version:del proyecto enprojects.yml(escrito literal como<node>; si el proyecto no lo declara, elnode-versiondel workflow del CI), víasource ~/.nvm/nvm.sh && nvm use <node>(el host tiene otro node por default). El CI puede correr con un node más nuevo que producción (hoy 22 vs 20 en algunos repos): un major de una dependencia de runtime (dependencies) cuyoengines.nodeexige más que producción se difiere aunque el CI dé verde; unadevDependency(jest, eslint, types…) nunca corre en producción y sólo necesita el node del CI. Los installs llevanPLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1(elpostinstallde algunos repos baja chromium; el CI también lo salta). - Lockfile y overrides.
package.jsonypackage-lock.jsonse commitean juntos y en sync (el deploy hacenpm ci); losoverridesdepackage.jsonse conservan (se elevan, nunca se borran: son el único pin de transitivos). Agregar un override nuevo vale SÓLO para cerrar una vuln transitiva quenpm audit fixno cierra (patrón ya existente en los repos) — nunca para forzar un peer range. - Sin
cddespués deEnterWorktree. El cwd del Bash persiste entre llamadas ycd ../absolutos no pasan el gate: todo comando se escribe relativo a la raíz del worktree (npm --prefix frontend …,npx --yes npm-check-updates --cwd frontend …,backend/.venv/bin/…,env --chdir=backend …). - Commits: inglés, Conventional Commits con el prefijo de la rama (tmpl §3/§8):
chore(deps): …para bumps,fix(deps): …para adaptaciones/pin-backs,revert(deps): …para reverts,docs: …para el reporte. SinCo-Authored-By: Claudeni footers de atribución de IA (regla explícita en losCLAUDE.mddel repo). Nuncagit reset --hard, nunca--no-verify; si un pre-commit hook falla, investigar y arreglar la causa raíz. --upgradees interactivo por naturaleza (dura horas y cruza turnos): nunca en cron/headless/barridos; no existe variante--all-*.
Cómo invocar este skill
Gating ($output-protocol §4): si el operador pasó cualquier argumento
(backend/frontend/--apply/--upgrade) → ejecutar directo, sin menú. Si la
intención es clara por la sesión (p.ej. acaba de llegar el aviso de un CVE puntual) →
proponer el comando en una línea y esperar confirmación. Sin argumentos → UNA
sola AskUserQuestion con Q1+Q2 fusionadas. Nunca preguntar en modo
fleet/headless/cron ni dentro de un barrido.
Q1 — Modo (selección única):
| label | description | preview |
|---|---|---|
| Auditar (Recommended) | read-only: escanea y arma el plan de bumps, no escribe nada | $vuln-audit |
Aplicar (--apply) |
aplica en batch los upgrades patch+minor — puede romper builds; checks mínimos, PR al final | $vuln-audit --apply |
Modernizar (--upgrade) |
secuencial por nivel semver: patch → minor → cada major en su commit; cada commit se pushea solo y espera su CI verde (N runs de CI, horas, cruza turnos); un major rojo se revierte y queda diferido | $vuln-audit --upgrade |
Q2 — Capa (selección única):
| label | description | preview |
|---|---|---|
| Ambas (Recommended) | backend (pip-audit) + frontend (npm audit) | $vuln-audit |
| Sólo backend | pip-audit + pip outdated sobre el venv | $vuln-audit backend |
| Sólo frontend | npm audit + npm outdated | $vuln-audit frontend |
Qué NO se pregunta: no hay más flags que estos dos ejes. --apply y --upgrade
son excluyentes y se tipean a propósito. Los majors no se eligen uno por uno en un
picker: sin --upgrade nunca se aplican (se evalúan read-only vía el menú
post-reporte); con --upgrade entran TODOS los que permiten los constraints, en
orden determinista, y es el CI —no un picker— quien decide cuáles quedan (rojo ⇒
revert + diferido). No existen --no-wait, --max-iterations ni --skip-majors:
el ritmo estricto y los topes de reintento son el diseño del modo. El git push +
PR no se ofrecen pre-run: --apply los hace al final, --upgrade con el primer
commit.
Detección de entorno (Fase 0)
-
Parsear
$ARGUMENTS: superficie (vacío/backend/frontend),APPLY=truesi trae--apply,UPGRADE=truesi trae--upgrade; los dos juntos ⇒ abortar. "Modos mutantes" =--applyy--upgrade. Sin ninguno la corrida es solo auditoría: los pasos marcados "(solo modos mutantes)" / "(solo--apply)" / "(solo--upgrade)" en todas las fases se saltan según corresponda. -
(solo modos mutantes) Guard de worktree (protocolo por sesión):
git rev-parse --show-topleveldebe caer bajo~/webapps/.wt/. Si cae en el clon principal, creá tu worktree de sesión ANTES de seguir — nunca busques una rama feature activa para hacerle checkout, ni crees una rama nueva en el clon principal. El slug depende del modo:vuln-auditpara--apply,deps-upgradepara--upgrade:# pre-entry: corre en el clon principal, antes de EnterWorktree OUT="$(bash "$HOME/webapps/vps-ops-toolkit/scripts/maintenance/session-worktree.sh" \ create chore vuln-audit)" # --upgrade: create chore deps-upgrade echo "$OUT" # PREFERIDO: imprime worktree=/branch=/base=/pr_base= — la BASE que # resuelve es la de la coordenada (release activa o default), nunca # la default a secas WT="$(sed -n 's/^worktree=//p' <<<"$OUT")"Entrá al worktree (Claude:
EnterWorktree path=$WT; Codex:cd "$WT") y confirmágit rev-parse --show-toplevelbajo~/webapps/.wt/antes de seguir. Si la sesión YA tiene su worktree/rama de un turno anterior, reutilizalo — no crees uno nuevo.--upgradecon un worktreedeps-upgradepreexistente: leésession-worktree.sh statusdesde adentro;pr_state=OPEN|none⇒ es un resume (ver## Idempotencia);pr_state=MERGED|CLOSED⇒ retiralo primero desde el clon principal (bash ~/webapps/vps-ops-toolkit/scripts/maintenance/session-worktree.sh remove deps-upgrade) y creá uno nuevo. Manual (sin el helper — evitar salvo emergencia; nunca derivar la BASE congit remote show origina secas, que da siempre la default e ignora una release activa — mismo bug que el CRITICAL de la Fase 3):# pre-entry: corre en el clon principal, antes de EnterWorktree SLUG="vuln-audit" # --upgrade: SLUG="deps-upgrade" REPO="$(basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")")" BASE="$(bash "$HOME/webapps/vps-ops-toolkit/scripts/maintenance/resolve-work-coordinate.sh" \ --check "$REPO" 2>/dev/null \ | awk -F= '$1=="pr_state"{ps=$2} $1=="resolved_branch"{rb=$2} END{if(ps=="single") print rb}')" [ -z "$BASE" ] && BASE="$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || echo master)" WT="$HOME/webapps/.wt/$REPO/$SLUG" git fetch origin "$BASE" --quiet git worktree add "$WT" -b "chore/$(date +%d%m%Y)-$SLUG" "origin/$BASE" -
(solo modos mutantes)
git status --porcelainya dentro del worktree → si imprime cualquier línea, abortar con: "Working tree no está limpio. Commitea antes de correr vuln-audit --apply/--upgrade (nunca stash en un clon principal del fleet)." Excepción de--upgrade: un tree sucio limitado a archivos de dependencias es una unidad a medias de una corrida anterior — ver## Idempotencia. -
Detectar superficies:
- Frontend:
[ -f frontend/package.json ]. - Backend:
[ -f backend/requirements.txt ].
- Frontend:
-
Si la superficie pedida es
backendy no hay backend → abortar. Idem para frontend. -
Entorno Python — dos reglas según el modo:
- Read-only (clon principal): detectar el venv del proyecto (el primero que exista:
backend/.venv/bin/python,backend/venv/bin/python; si ninguno existe y se va a auditar backend, abortar pidiendo crear el venv) y usarlo sólo para leer (pip list --outdated,pip-audit). Lo único que se instala ahí espip-auditsi falta (contrato conscripts/lib/deps-probes.sh, que sólo lo usa si ya está):backend/venv/bin/pip show pip-audit >/dev/null 2>&1 || backend/venv/bin/pip install pip-audit - Modos mutantes (worktree): el venv del clon principal es el de producción y no
está enlazado. Crear uno propio dentro del worktree (gitignored) — y recrearlo
desde cero si ya existía de una corrida anterior (un venv que quedó con versiones
distintas a
requirements.txthace mentir apip list --outdated):rm -rf backend/.venvpython3 -m venv backend/.venv
El intérprete es elbackend/.venv/bin/pip install -q -r backend/requirements.txt && backend/.venv/bin/pip install -q pip-auditpython-versiondel workflow del CI (grep -n python-version .github/workflows/ci.yml); si elpython3del host no coincide, usarpython3.<minor>si existe, y si no, ⏸️ (no se crea el venv con otro major.minor). En las fases siguientes,<venv>esbackend/venv(obackend/.venv) en read-only ybackend/.venven los modos mutantes; se escribe literal en cada comando.
- Read-only (clon principal): detectar el venv del proyecto (el primero que exista:
-
Rama base: en los modos mutantes sale de
session-worktree.sh status(base= la de la coordenada: release activa si la hay, default si no) y se escribe literal (<base literal>). En read-only,git remote show origin | grep "HEAD branch"(o probarorigin/main/origin/master) alcanza para informar. -
Capturar
BASE_SHAcongit merge-base HEAD origin/<base literal>(short) — sólo informativo para el reporte. -
Leer
CLAUDE.mdyAGENTS.mdraíz si existen, y ademásgrep -n "#" backend/requirements.txt(los comentarios inline son constraints, p.ej. «Production uses MySQL 8.0.x; Django 6.1 requires MySQL 8.4+» ⇒ Django<6.1), elnode_version:del proyecto en~/webapps/vps-ops-toolkit/projects.yml(el de producción =<node>literal para todo install/build; si falta, elnode-versiondel workflow) ypython-version/node-versionde.github/workflows/ci.yml, para detectar:- Pin policies adicionales (ej. "cryptography pinned <44.0").
- Slice de test mínimo recomendado (sólo se usa si el proyecto tiene
settings_testsqlite enbackend/pytest.ini). - Cualquier comando custom de build/test que corra el CI.
-
<proyecto>(el nombre con el que se bautizan los archivos de/tmp) es elproject=que imprimiósession-worktree.sh status, y se escribe literal en cada comando:${PROJ}es expansión de parámetros —rechazo medido de Gate A dentro de un worktree— y además una variable que no sobrevive entre bloques. -
(solo
--upgrade) Preflight. Todo debe pasar antes de tocar nada; cualquier fallo es🚫 vuln-audit REFUSED (<razón>)o⏸️, nunca se sigue:Check Comando (una llamada) Falla ⇒ CI presente ls .github/workflowsygh run list --limit 3— la duración del último run es la estimación por unidadT🚫 REFUSED (sin CI: --upgrade exige un check verde por commit; usá --apply)Base verde gh run list --branch <base literal> --limit 1conconclusion=success🚫 REFUSED (base roja: el CI no puede arbitrar)Sin otro PR de dependencias abierto gh pr list --state open --json number,headRefName,title— cualquier rama*vuln-audit*,*deps-upgrade*,dependabot/*o título condeps/dependenc🚫 REFUSED (PR de dependencias #<n> abierto: drenalo con $merge-queue primero — conflictos de lockfile)Node de producción disponible source ~/.nvm/nvm.sh && nvm ls <node>⏸️connvm install <node>para el operadorLockfile en sync + node_modulesdel worktreesource ~/.nvm/nvm.sh && nvm use <node> && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm --prefix frontend ci🚫 REFUSED (package-lock fuera de sync: arreglalo en un PR aparte)Presupuesto imprimir ℹ️ K unidades estimadas × T ≈ <h>; peor caso por major 4T(sin pregunta: el flag fue explícito)—
Fase 1 — Frontend
Ejecutar solo si la superficie ∈ {ambas, frontend} y frontend/package.json existe.
Los pasos 1–2 son la auditoría (siempre); el 0 y del 3 en adelante son la aplicación
en batch (solo --apply). En --upgrade la aplicación es el loop de
## Modo --upgrade (los pasos 3–7 no corren).
-
(solo
--apply) Bootstrap del worktree (en--upgradeya lo hizo el preflight): sinnode_modules,npm outdatedreporta todo como MISSING.source ~/.nvm/nvm.sh && nvm use <node> && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm --prefix frontend ci -
Snapshot inicial:
npm --prefix frontend audit --json > /tmp/<proyecto>-npm-audit.json || truenpm --prefix frontend outdated --json > /tmp/<proyecto>-npm-outdated.json || truenpm outdatedretorna exit 1 cuando hay outdated; eso es esperado, no es error. En un clon de deploy estático (sinnode_modules, p.ej. projectapp)npm outdatedmarca MISSING:currentse toma depackage-lock.json(packages."node_modules/<pkg>".version) — no se instala nada en el clon principal. -
Parsear y clasificar:
- De
npm-audit.json: lista de paquetes con{package, severity, notes}y totales{critical, high, moderate, low}. - De
npm-outdated.json: por cada directo{current, wanted, latest}. Clasificar por el diffcurrent → latest(nunca porwanted, que depende del rango^):patch(mismo major.minor),minor(mismo major ≠ 0, distinto minor),major(distinto major, o0.x → 0.y, o0.x → 1.x). Guardar la lista literalZEROX= directos cuyocurrentes0.x.
Corte por modo: sin flags la fase termina acá — lo parseado alimenta la tabla del plan (Fase 3) y no se toca
package.jsonni se corre ningún install. Con--upgrade, la aplicación es el loop de## Modo --upgrade. - De
-
(solo
--apply) Aplicar updates (un comando por llamada;--rejectsólo en la corrida minor y sólo siZEROXno está vacía; nuncanpm audit fix --force):npm --prefix frontend audit fixnpx --yes npm-check-updates --cwd frontend -u --target minor --reject <ZEROX literal, separado por comas>source ~/.nvm/nvm.sh && nvm use <node> && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm --prefix frontend install -
(solo
--apply) Manejo de ERESOLVE: sinpm installfalla conERESOLVE:- Identificar el paquete ofensor del mensaje de error.
- Editar
frontend/package.json(Edit) para revertir ese paquete a la última versión que respete las peer deps actuales (típicamente, retroceder 1 minor o quedarse en la versión previa alncu). - Repetir el
npm --prefix frontend installde arriba. - Registrar el caso en la sección
Rollbacksdel reporte. - Si el reintento falla otra vez, abortar con error claro. Nunca
--legacy-peer-depsen el commit.
-
(solo
--apply) Verificar:npm --prefix frontend auditsource ~/.nvm/nvm.sh && nvm use <node> && npm --prefix frontend run build(o el script de build que corre el CI, si es otro:
grep -n "npm run" .github/workflows/ci.yml). Si el build falla:- Si ya se hizo commit y aún no se pushó,
git reset --soft HEAD~1. - Reportar el error y abortar.
- Si ya se hizo commit y aún no se pushó,
-
(solo
--apply) Commit (sin Co-Authored-By, sin footers de IA):git add frontend/package.json frontend/package-lock.jsongit commit -m "chore(deps): frontend patch+minor updates"Si
npm installno produjo cambios enpackage.json/package-lock.json, no commitear; registrar en el reporte que no había updates aplicables. -
(solo
--apply) Capturar el snapshot final (npm --prefix frontend audit --jsonpost-update) para la secciónUpdates Applieddel reporte.
Fase 2 — Backend
Ejecutar solo si la superficie ∈ {ambas, backend} y backend/requirements.txt existe.
Los pasos 1–4 son la auditoría (siempre); del 5 en adelante es la aplicación en batch
(solo --apply). En --upgrade la aplicación es el loop de ## Modo --upgrade.
-
Entorno: el
<venv>de la Fase 0 paso 5 — el del proyecto (sólo lectura) en read-only,backend/.venvdel worktree en los modos mutantes. Nuncasource …/activate: los binarios se invocan por ruta. -
pip-audit disponible: read-only, instalado en el venv del proyecto si faltaba (Fase 0 paso 5); modos mutantes, ya instalado en
backend/.venv. -
Snapshot inicial (
pip-auditsale 1 cuando hay vulns; esperado). Con<venv>literal (backend/venvobackend/.venvsegún el modo):backend/venv/bin/pip-audit --format json --progress-spinner=off > /tmp/<proyecto>-pip-audit.json || truebackend/venv/bin/pip list --outdated --format json > /tmp/<proyecto>-pip-outdated.jsonLa verdad de las versiones declaradas es
requirements.txt; el venv sólo aportalatesty las vulns de lo instalado. -
Construir el plan:
- Parsear
requirements.txtlínea por línea: capturar pin actual de cada paquete (==X.Y.Z,>=A,<B, sin pin, etc.) y los comentarios inline (constraints). - Para cada paquete outdated: clasificar
patch/minor/majorpor el diffcurrent → latest(0.x → 0.yes major; para Django también lo es una feature releaseX.Y → X.Z); calculartarget= última versión que respete el pin y los constraints documentados (ej. pin<44.0⇒ target ≤ 43.x; comentario «Django 6.1 requires MySQL 8.4+» ⇒ Django<6.1). En--applyel target además no cruza el major actual. Sitarget == current⇒ skip, y si lo que lo frena es un constraint, marcarconstraint: <cita>. - Para cada paquete con vulns que solo se arreglan fuera del pin/constraint: marcarlo como remaining en el reporte (no intentar el bump).
Corte por modo: sin flags la fase termina acá — el plan alimenta la tabla de Fase 3 y no se edita
requirements.txtni se instala nada en el venv del proyecto (más allá del propiopip-audit). Con--upgrade, la aplicación es el loop de## Modo --upgrade. - Parsear
-
(solo
--apply) Aplicar: editarbackend/requirements.txt(Edit) con las nuevas versiones (mantener el operador del pin: si era==, sigue==<nuevo>; si era rango, ajustar el floor sin tocar el techo). Luego:backend/.venv/bin/pip install -r backend/requirements.txt && backend/.venv/bin/pip checkbackend/.venv/bin/pip-audit --format json --progress-spinner=off > /tmp/<proyecto>-pip-audit-final.json || true -
(solo
--apply) Verificar (sin base de datos):env --chdir=backend .venv/bin/python manage.py checkenv --chdir=backend .venv/bin/python -m pytest --collect-only -qmanage.py checkdebe imprimir "0 issues" (los checks corren con los settings por defecto delmanage.py, nunca conDJANGO_ENV=production);--collect-onlydebe colectar sin errores. Slice mínimo — sólo sigrep -n settings_test backend/pytest.iniimprime algo (settings de test sqlite): el comando de test ejemplar delCLAUDE.mdo el primertests/test_*.py, conenv --chdir=backend .venv/bin/python -m pytest <archivo literal> -q. Alternativa equivalente: sigrep -n DJANGO_DB_ENGINE backend/*/settings.pymuestra que el motor se lee del entorno, el mismo comando conenv --chdir=backend DJANGO_DB_ENGINE=django.db.backends.sqlite3 .venv/bin/python -m pytest <archivo literal> -qfuerza sqlite (el archivo de test queda en el worktree, nunca en la base real). Si no se cumple ninguna de las dos, registrar «⏭️ slice: los settings de test no son sqlite — la suite la corre el CI» y no correr ningún test (con el.envde producción enlazado sería un test contra la base real).Si cualquier verificación falla:
- Si ya se hizo commit y aún no se pushó,
git reset --soft HEAD~1. - Reportar el comando que falló y abortar.
- Si ya se hizo commit y aún no se pushó,
-
(solo
--apply) Commit:git add backend/requirements.txtgit commit -m "chore(deps): backend patch+minor updates"Si
requirements.txtno cambió, no commitear.
Modo --upgrade — modernización secuencial (un PR, un commit por unidad, CI verde por commit)
Corre después de la Fase 0 (con el preflight ✅) y de los snapshots/planes de las Fases 1–2
(pasos de auditoría), ya dentro del worktree deps-upgrade, con node_modules y
backend/.venv propios. Las Fases 1–2 NO aplican nada en este modo: la aplicación es este
loop. Cada unidad se pushea sola y nada avanza sin su CI verde.
Unidades y orden (determinista)
| Unidad | Contenido | Subject del commit |
|---|---|---|
lockfile(frontend) |
npm audit fix (transitivos in-range, sin tocar directos) |
chore(deps): frontend transitive audit fixes (lockfile) |
patch(<superficie>) |
todos los directos con bump patch disponible | chore(deps): <frontend|backend> patch updates (<N> pkgs) |
minor(<superficie>) |
todos los directos con bump minor (excluidos los 0.x) |
chore(deps): <frontend|backend> minor updates (<N> pkgs) |
major(<grupo>) |
UN paquete (o grupo lockstep) a la mayor versión que permiten los constraints | chore(deps): <pkg> <old> -> <new> · grupo: … (+react-dom, @types/react) |
Body de todo commit de bump: una línea por paquete <pkg> <old> -> <new> (varios -m).
Unidad sin diff ⇒ ⏭️ sin commit ni run.
Orden: frontend → backend; dentro de cada superficie: lockfile → patch → minor → majors. Majors: framework → tooling → resto alfabético.
| Superficie | Framework (grupos lockstep) | Tooling (grupos) |
|---|---|---|
| frontend | react, react-dom, @types/react, @types/react-dom · next, eslint-config-next · nuxt, @nuxt/* · vue, vue-router, pinia, @vue/* |
typescript · eslint, @eslint/js, @typescript-eslint/*, eslint-plugin-*, eslint-config-* · prettier (+ eslint-config/plugin-prettier) · vite, @vitejs/* · vitest, @vitest/* · jest, ts-jest, babel-jest, jest-environment-jsdom, @types/jest · @playwright/test, playwright · tailwindcss, @tailwindcss/*, postcss, autoprefixer · @testing-library/* |
| backend | Django (+ los django-* que pip check declare incompatibles: bump lockstep permitido dentro de la unidad) |
pytest, pytest-django, pytest-cov, coverage, ruff, black, flake8, mypy, isort |
@types/<x> viaja siempre con <x>. Django: cada feature release (6.0 → 6.1) es una unidad
major de framework aunque el primer número no cambie (nunca va en el batch minor: es donde se
pierde el gate de la base de producción — caso real xpandia PR #70). Un major cuyo objetivo viola un constraint documentado
(comentario en requirements.txt, pin en CLAUDE.md/AGENTS.md, engines.node mayor que
el node_version de producción en projects.yml — sólo para dependencies de runtime, no para
devDependencies —, Requires-Python mayor que el python del CI) no se intenta: fila ⏭️ constraint: <cita>. Si existe una versión permitida menor
que igual es major (p.ej. Django 4.2 → 6.0 con techo <6.1), ése es el objetivo. Versiones
disponibles: backend/.venv/bin/pip index versions <pkg> · npm view <pkg>@<ver> engines.node.
Paquetes runtime-only que el CI no ejercita (gunicorn/uvicorn, workers huey/celery,
mysqlclient, codecs de Pillow) se aplican igual pero llevan la nota CI: no cubre en el
ledger y van a «Acción operativa posterior al merge» del reporte.
Pre-loop
- Snapshots de auditoría (Fase 1 pasos 1–2, Fase 2 pasos 3–4) ya en el worktree.
- Armar la lista de unidades
1..Kcon paquetes y objetivos; imprimirla con la estimaciónK × T(T = duración del último run, del preflight). - Resume (
## Idempotencia): reconciliar congit log --format='%h %s' origin/<base literal>..HEADy saltar las unidades ya aplicadas o diferidas. - Desde la segunda unidad, guard antes de cada push:
gh pr view <n> --json state -q .statedebe imprimirOPEN.
Loop por unidad k (un comando simple por llamada, sin cd)
Paso 1 — precondición: git status --porcelain vacío y el último CI de HEAD visto en ✅.
Paso 2 — aplicar la unidad. Frontend, según el tipo (lockfile / patch / minor / major):
npm --prefix frontend audit fix
npx --yes npm-check-updates --cwd frontend -u --target patch --filter <directos clasificados patch, separado por comas>
npx --yes npm-check-updates --cwd frontend -u --target minor --filter <directos clasificados minor, separado por comas> --reject <ZEROX literal, separado por comas>
(los batches llevan --filter con la lista literal de los directos clasificados en la Fase 1 paso 2
— sin filtro, --target patch movería a un patch intermedio a los paquetes clasificados minor/major y
gastaría un ciclo de CI de más; lista vacía ⇒ la unidad es ⏭️ sin correr ncu)
npx --yes npm-check-updates --cwd frontend -u --target latest --filter <grupo literal, separado por comas>
(major con techo ⇒ editar frontend/package.json con Edit conservando operador y sección,
sin ncu). Sonda previa opcional para un major: npm --prefix frontend install <pkg>@<ver> --dry-run
corre el resolver sin tocar archivos. Leé la SALIDA, no el rc: para un paquete pedido desde la raíz
npm suele resolver con npm warn ERESOLVE overriding peer dependency y rc 0 (el peer de un plugin
transitivo queda sobreescrito); sólo un npm ERR! ERESOLVE difiere la unidad ahí mismo con el
bloqueador literal. Con warnings la unidad sigue y el veredicto lo dan el smoke y el CI. Luego, siempre:
source ~/.nvm/nvm.sh && nvm use <node> && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm --prefix frontend install && npm --prefix frontend ls --depth=0
Backend: editar backend/requirements.txt con Edit (== → ==<new>; >=A,<B: patch/minor
sube el piso y conserva el techo, major sube el techo al próximo major Y el piso a la nueva
versión; sin techo: sube el piso) y:
backend/.venv/bin/pip install -r backend/requirements.txt && backend/.venv/bin/pip check
git status --porcelain vacío tras aplicar ⇒ unidad vacía ⇒ ⏭️, siguiente. ERESOLVE en un
batch ⇒ restaurar el spec del ofensor con Edit, reinstalar, seguir; el excluido se reintenta
UNA vez como unidad minor-retry después de los majors (suele destrabarse con un peer
major); si vuelve a fallar ⇒ diferido ERESOLVE con <peer>. pip check rojo en un batch ⇒
pin-back del par en conflicto antes de commitear; en un major ⇒ bump lockstep del compañero
que declara la incompatibilidad, y si no alcanza ⇒ diferir.
Paso 3 — smoke local (≤3 comandos, nunca la suite):
| Superficie | Siempre | Tercer slot según unidad |
|---|---|---|
| frontend | source ~/.nvm/nvm.sh && nvm use <node> && npm --prefix frontend run build (o el script de build del CI) |
eslint/prettier/typescript ⇒ npm --prefix frontend run lint (si el script existe) — gatea sólo si el lint estaba limpio en HEAD antes de la unidad; si ya era rojo (medido una vez al inicio: N problems), gatea el conteo (no debe crecer) y un crash del lint (TypeError, Error while loading rule) sigue siendo rojo · jest/testing-library ⇒ un solo spec: npm --prefix frontend exec -- jest <spec literal> · playwright ⇒ npm --prefix frontend exec -- playwright --version |
| backend | env --chdir=backend .venv/bin/python manage.py check · env --chdir=backend .venv/bin/python -m pytest --collect-only -q — si el settings del proyecto lee el motor del entorno (linaje base_feature: DJANGO_DB_ENGINE), TODOS los manage.py/pytest del worktree llevan DJANGO_DB_ENGINE=django.db.backends.sqlite3 DB_NAME=/tmp/<proyecto>-deps-upgrade.sqlite3 delante (con el .env de prod enlazado, hasta un check plano abre la conexión a MySQL: medido en mimittos, Django 6.1 lo rechazó desde el check) |
major de Django o de mysqlclient ⇒ env --chdir=backend .venv/bin/python manage.py makemigrations --check --dry-run && env --chdir=backend DJANGO_ENV=production .venv/bin/python manage.py check --database default — el segundo abre la conexión a la base de PRODUCCIÓN con el selector de prod del proyecto (DJANGO_ENV=production o DJANGO_SETTINGS_MODULE=<settings_prod>, el que use su manage.py/systemd) y corre los checks del backend MySQL sin escribir: es lo único con settings de producción permitido desde el worktree, porque el CI corre sobre sqlite y no puede ver una incompatibilidad Django↔MySQL (caso real: xpandia PR #70 verde → hotfix #71, 2026-08-26). Si no es un major de Django/mysqlclient, el slice sólo con settings_test sqlite (o forzando DJANGO_DB_ENGINE=django.db.backends.sqlite3 si el settings lo lee del entorno) |
Smoke rojo ⇒ fix local ≤2 intentos con el mismo alcance del Paso 7(d), sin gastar CI — uno de
los intentos puede ser bajar al major intermedio cuando el objetivo está varios majors por
delante (p.ej. typescript 5 → 7 rojo ⇒ probar 6.x; queda como chore(deps): typescript 5.9.3 -> 6.0.3
con la nota «7.x: »). Si no cierra: descartar sólo los cambios no commiteados de la unidad
(git checkout -- frontend/package.json frontend/package-lock.json o
git checkout -- backend/requirements.txt — nunca reset --hard, nunca checkout .),
resincronizar (npm --prefix frontend ci / pip install -r), registrar
<pkg> <old> -> <new> | smoke local: <causa> en /tmp/<proyecto>-deps-upgrade-deferred.txt,
ledger 🔁 diferida, siguiente unidad.
Paso 4 — commit:
git add frontend/package.json frontend/package-lock.json
git commit -m "chore(deps): frontend patch updates (12 pkgs)" -m "axios 1.7.2 -> 1.7.9" -m "dayjs 1.11.10 -> 1.11.13"
(backend: git add backend/requirements.txt).
Paso 5 — push (+ PR la primera vez). Rama y base salen de session-worktree.sh status
y van literales:
git push -u origin <branch literal>
gh pr create --base <base literal> --title "chore(deps): sequential dependency upgrade (<DDMMYYYY>)" --body "Sesión: <sesión>
Intención: modernización secuencial de dependencias (--upgrade): un commit por unidad semver, CI verde por commit, majors rojos revertidos y diferidos
Plan de unidades:
1. frontend lockfile (npm audit fix)
2. frontend patch (<N> pkgs)
3. frontend minor (<M> pkgs)
4. <pkg> <old> -> <new> (+<compañeros>)
..."
El body es texto literal con saltos de línea reales — nunca --body "$(printf …)". El CI de
la rama corre por el PR (pull_request), no por el push: el PR se crea inmediatamente
después del primer push. Unidades siguientes: git push a secas, con el guard del pre-loop
antes.
Paso 6 — esperar el CI (contrato de $pr-green Phase 3):
gh pr checks <n> --watch --fail-fast=false
Llamada foreground con timeout: 600000 en el tool Bash, directa — sin sleep previo: el Bash de
Claude Code rechaza un sleep en foreground. «no checks reported» (el PR se creó hace segundos)
⇒ gh run list --branch <branch literal> --limit 1 y un reintento del --watch; si sigue sin
checks ⇒ ⏸️ «CI no disparó para el PR». En Claude Code un --watch que supera los 600 s pasa solo a background («moved to the
background (ID: …) — You will be notified when it completes»): no montes nada, esperá esa
notificación y clasificá (Paso 7). Si en cambio muere con exit 143 (Codex, u otro harness sin
auto-background; le pasa a todo repo con T > 8 min — ahí montá el watcher directo, sin foreground)
⇒ NO re-bloquees con otro --watch: barré tus watchers obsoletos (TaskStop sólo de tasks
CI-MONITOR [<repo>#<n> upgrade …] de esta sesión) y montá un solo comando en
background (Bash run_in_background: true; sin loops ni sustituciones — la variante
canónica de $merge-when-green usa for + $(…) y el gate del worktree la rechaza):
gh pr checks <n> --watch --fail-fast=false --interval 30 && echo "CI-MONITOR [<repo>#<n> upgrade u<k>] verde — NEXT: clasificar con gh pr checks <n> --json name,state,bucket y seguir con la unidad <k+1>" || echo "CI-MONITOR [<repo>#<n> upgrade u<k>] rojo — NEXT: gh pr checks <n> --json name,state,bucket y fix loop de la unidad <k>"
Reportá la plantilla intermedia de ## Output final y cortá el turno. Al llegar la
notificación con CI-MONITOR [: clasificá SIEMPRE con --json (el echo sólo transporta la
etiqueta), re-verificá gh pr view <n> --json state -q .state y seguí el loop sin
reconstruir contexto. Máximo un watcher por PR.
Paso 7 — clasificar:
gh pr checks <n> --json name,state,bucket
- Todos
pass⇒ ledger ✅, unidadk+1. - Algún
fail⇒ rojo:- (a) Base verde primero:
gh run list --branch <base literal> --limit 1— base roja ⇒ ⏸️ «rojo de la base, no de la unidad». - (b) Logs:
gh run list --branch <branch literal> --limit 1 --json databaseId,status,conclusion→gh run view <run-id> --log-failed. Extraer los IDs (pytestFAILED path::Clase::test, jestdescribe > it+ archivo, playwright spec + título) y la clase del error (import/API renombrada, tipos, config de build/test, lint, migración). - (c) Infra (runner perdido,
timed_out, 5xx de registry, sin logs) ⇒gh run rerun <run-id> --faileduna vez → Paso 6; segundo infra ⇒ ⏸️. - (d) Unidad major — fix loop ≤2 iteraciones (cada una = commit
fix(deps): adapt <area> to <pkg> <new>+git push+ Paso 6). Con el ritmo estricto el único delta desde el último verde es la unidad, así que el rojo es atribuible por construcción. Adaptaciones permitidas sin pausar (son la esencia del major): bajar al major intermedio como iteración (fix(deps): step <pkg> down to <ver> (<causa>)), imports y APIs renombradas, configs de build/test/lint (jest.config,vite.config,eslint.config,tsconfig,pytest.ini), tipos, settings renombrados documentados en las release notes, bumps de compañeros lockstep, elevar unoverride. Prohibidas ⇒ revert inmediato + diferido: modelos/migraciones (makemigrations --checksucio), settings de seguridad, auth, pagos/facturación, cambios de datos, o un diff de código de producción~50 líneas (excede el esfuerzo acotado). Tercer rojo ⇒ revert (Paso 8).
- (e) Unidad batch — pin-back: el log nombra al ofensor ⇒ restaurar su spec exacto con
Edit,
npm --prefix frontend install/pip install -r, commitfix(deps): pin <pkg> back to <old> (CI red: <cause>), push, Paso 6; ≤2 pin-backs por batch. No atribuible ⇒ ⏸️ con el extracto del log y las dos salidas para el operador:git revertdel batch, o bisección por mitades en dos unidades nuevas (no se automatiza).
- (a) Base verde primero:
- Algún
cancel⇒ comparargit ls-remote origin refs/heads/<branch literal>congit rev-parse HEAD: si difieren, alguien pusheó a la rama ⇒ ⏸️ «rama tocada por otro actor» (los runs de las unidades anteriores tienen otros SHA por diseño — no son ajenos). Si coinciden, elcanceles del propio run: un job que pega sutimeout-minutes(runners lentos; el E2E de xpandia lo hizo a los 18 min exactos) GitHub lo reportacancelledy cuenta como infra ⇒gh run rerun <run-id> --faileduna vez; segundo cancel ⇒ ⏸️. pendingresidual ⇒ un--watchmás.
Paso 8 — revert de un major. <sha> = commit de la unidad (del
git log --format='%h %s' origin/<base literal>..HEAD); el rango cubre la unidad y sus fixes:
git revert --no-commit <sha unidad literal>^..HEAD && git commit -m "revert(deps): react 18.3.1 -> 19.1.0 (CI red: <cause>)"
Conflicto ⇒ git revert --abort y ⏸️. Después: resincronizar el entorno local
(npm --prefix frontend ci / backend/.venv/bin/pip install -r backend/requirements.txt),
git push, Paso 6 (restaura un estado ya verde: rojo ⇒ infra/flaky ⇒ rerun una vez ⇒ ⏸️),
ledger 🔁 diferida (<causa>), registrar en /tmp/<proyecto>-deps-upgrade-deferred.txt,
siguiente unidad. Si el diferimiento viene del check contra la base de producción (p.ej.
«MySQL 8.4 or later is required»), dejá el constraint documentado como comentario inline
en backend/requirements.txt dentro del mismo commit de revert — o, si se difirió en el smoke sin
llegar a commitear, dentro del commit docs: del cierre (# Production uses MySQL 8.0.x; Django 6.1 requires MySQL 8.4+): la próxima corrida lo salta por constraint en vez de
gastar un ciclo de CI.
Paso 9 — ledger (fila en la respuesta; la fuente de verdad es git log):
| # | Unidad | Paquetes (antes → después) | Commit | CI run | Iter. fix | Resultado | con
Resultado ∈ {✅ aplicada · ⏭️ vacía / constraint: <cita> · 🔁 revertida/diferida ()
· ⏸️ en vuelo · pendiente}; los runtime-only llevan además CI: no cubre.
Cierre
Snapshots finales (npm --prefix frontend audit --json, npm --prefix frontend outdated --json,
backend/.venv/bin/pip-audit …, backend/.venv/bin/pip list --outdated …) →
audit-report.md (plantilla en Fase 3, ### Modo --upgrade — reporte final) → commit
docs: dependency upgrade report (<YYYY-MM-DD>) → git push → Paso 6 (última espera: el
DoD de sesión es PR abierto con CI verde en HEAD; si expira, watcher con NEXT: emitir el veredicto final) → opcional pero recomendado: gh pr edit <n> --body "…" con el ledger final
como texto literal, conservando las líneas Sesión:/Intención: (lo consume $merge-queue)
→ PR URL: <url> y PARÁ (merge = $merge-queue/operador).
Racionalizaciones que NO valen
| Excusa | Realidad |
|---|---|
| «El CI tarda 30 min; pusheo dos unidades juntas» | Se pierde la atribución: el ritmo estricto ES el diseño. Una unidad por push. |
| «Amend + force push para dejar un commit por cambio» | Force push denegado en toda forma (capa 1). Commit nuevo o git revert. |
| «Corro la suite local para no gastar CI» | Nunca la suite local: el CI es el árbitro y el .env del worktree es el de producción. |
| «El venv del clon principal ya tiene todo instalado» | Es el venv del servicio en producción. Venv propio en el worktree, siempre. |
«ncu -u a latest de una vez y listo» |
Un rojo no se puede atribuir. Batch patch → batch minor → un major por commit. |
| «Este major sólo necesita una migración chica» | Migraciones = fuera de alcance: revert y diferido con causa. |
«cd frontend y sigo desde ahí» |
El cwd persiste entre llamadas y cd .. no pasa el gate: comandos relativos a la raíz (--prefix, --cwd, env --chdir). |
| «La suite local corre sobre sqlite, no toca prod» | Sigue siendo la suite completa (regla de casa): el CI la corre por cada commit. Local = smoke ≤3 comandos. |
«manage.py check con settings de prod es sólo lectura y atrapa lo que el CI no ve» |
Cierto, y por eso está permitido — SÓLO check --database default, SÓLO en majors de Django/mysqlclient. Todo lo demás con settings de producción sigue prohibido. |
| «Un paquete por commit es más trazable» | La granularidad es por nivel semver (decisión del operador): batches patch/minor, majors uno por uno. Un rojo en un batch se resuelve con pin-back. |
| «Con 20 minutos alcanza para 2-3 commits; después sigo» | Correcto: el loop se reanuda desde git (## Idempotencia) — pero cada corte deja el DoD cumplido (PR abierto + CI verde en HEAD), nunca un push sin veredicto. |
«El repo no prohíbe Co-Authored-By; un commit viejo lo lleva» |
Los commits de esta skill no llevan footers de IA (regla de los CLAUDE.md del fleet). Un commit ajeno que los tenga no es precedente. |
| «Mientras corre el CI preparo el próximo commit» | El tree tiene que estar limpio al recibir el veredicto: un rojo se resuelve sobre HEAD limpio (pin-back/revert). Esperar ES parte del ritmo. |
«git commit -am es seguro, sólo cambió requirements.txt» |
Siempre git add por path: el ledger/reporte y otros archivos pueden estar sucios sin que lo notes. |
| «Aprovecho y le pongo techo a los rangos abiertos» | El estilo del pin es del operador: == sigue ==, >=A,<B conserva su forma, un rango sin techo sólo sube el piso. |
«Agrego overrides para que los peers acepten el major» |
Un override que fuerza un peer range es --legacy-peer-deps disfrazado. ERESOLVE ⇒ diferido con el bloqueador literal; los overrides existentes sólo se elevan. |
| «Preparo todos los commits en local y los pusheo de a uno» | Un rojo en la unidad k deja k+1..n apilados sobre un estado roto (fixes y reverts se entrelazan). El loop es aplicar → push → verde → siguiente. |
| «Uso el node/npm del PATH del host; el CI es el árbitro» | Otro major de npm normaliza el lockfile distinto (diffs ruidosos, npm ci del CI puede fallar). Siempre nvm use <node>. |
Fase 3 — Plan / Reporte
Modo default (sin flags) — el plan en la respuesta, nada escrito
Consolidar lo parseado en Fases 1–2 en la tabla del plan de bumps (esto ES el entregable de la corrida default; se muestra en la respuesta, no se escribe ningún archivo ni commit):
| Paquete | Superficie | Actual → Propuesta | Severidad (vulns) | Tipo | Unidad --upgrade |
Riesgo |
|---|---|---|---|---|---|---|
| ... | frontend/backend | X.Y.Z → X.Y.W |
critical/high/moderate/low/— | patch / minor / major (skip en --apply) | lockfile / patch-batch / minor-batch / major #k / constraint: <cita> |
breve: pin techo, peer deps, breaking changes conocidos |
- Los majors se listan igual, marcados
major (skip en --apply)—--applynunca los aplica;--upgradelos aplica uno por commit (columnaUnidad). Riesgo: una frase por paquete (qué podría romper el bump o por qué se saltea).- Debajo de la tabla, la lista «Orden de unidades para
--upgrade» (1..K, con paquetes y objetivos): el plan read-only es el dry-run exacto del modo. - La aplicación queda para
--apply/--upgradeo para la selección en el menú post-reporte.
Modo --apply — reporte y commit final
Se ejecuta siempre que hubo --apply (incluso si una fase no produjo updates: el reporte lo refleja).
-
Generar
audit-report.mden la raíz del proyecto, sobrescribiendo si existe. Plantilla:# Vulnerability Audit & Dependency Update Report **Branch:** <git rev-parse --abbrev-ref HEAD> **Date:** <YYYY-MM-DD> **Base:** <BASE_BRANCH> @ <BASE_SHA> **Scope:** patch + minor updates only (no major version bumps) ## Summary | Surface | Vulns (initial) | Outdated (initial) | |----------|-----------------|--------------------| | Frontend | <total / breakdown por severity> | <count> | | Backend | <total across N packages> | <count> | --- ## Frontend — `npm audit` (initial) Source: `/tmp/<PROJ>-npm-audit.json` | Package | Severity | Notes | |---|---|---| | ... | ... | ... | **Totals:** <crit>/<high>/<mod>/<low>. ## Frontend — `npm outdated` (initial) Source: `/tmp/<PROJ>-npm-outdated.json` - <pkg>: <current> → <wanted> → <latest> *(skip si major)* --- ## Backend — `pip-audit` (initial) Source: `/tmp/<PROJ>-pip-audit.json` | Package | Current | Vulns | Min in-major fix | |---|---|---|---| ## Backend — `pip list --outdated` (initial) Source: `/tmp/<PROJ>-pip-outdated.json` - <pkg> <current> → <latest> *(constrained / major bump skipped si aplica)* --- ## Plan ### Frontend - <bumps planeados> ### Backend - <bumps planeados, respetando pins> ## Updates Applied ### Frontend (commit `chore(deps): frontend patch+minor updates`) - <pkg> <old> -> <new> - Final `npm audit`: <totales>. - Remaining outdated (majors saltados intencionalmente): <lista>. ### Backend (commit `chore(deps): backend patch+minor updates`) - <pkg> <old> -> <new> - `pip-audit` final: <total remaining> en <N> paquetes (todos requieren majors saltados o están fuera de pin). ## Rollbacks - <si hubo, descripción + razón. Si no, "Ninguno."> ## Verification Results ### Frontend - `npm audit`: <totales>. - `npm run build`: <success / detalle>. ### Backend - `python manage.py check`: <output resumido>. - `pytest --collect-only`: <N tests collected, errors>. - Slice: `<comando>`: <N passed> (o «⏭️ sin settings_test sqlite»).Si una superficie no se auditó (por
$ARGUMENTS), omitir sus secciones. -
Commit del reporte:
git add audit-report.mdgit commit -m "docs: vulnerability audit report (<YYYY-MM-DD>)" -
Push + PR al primer push (protocolo por sesión, tmpl §9). Las variables de la Fase 0 no persisten entre bloques bash (sólo el cwd), y dentro del worktree Claude rechaza el comando compuesto: se relee el registro. La BASE del PR es la de la COORDENADA, nunca la default a secas: un PR con
base=defaulten un repo con release activa es por definición un candidato a release, y flipearíapr_stateaambiguousen el próximoresolve-work-coordinate.sh— receta canónica, la misma de $pr-green Phase 2 / tmpl §0:bash ~/webapps/vps-ops-toolkit/scripts/maintenance/session-worktree.sh statusDe ahí salen
branchybase(esebaseYA es el de la coordenada: la release activa cuando la hay, la default sólo si no la hay). Se escriben literales — post-EnterWorktreeno hay$(...)que los reconstruya:git push -u origin <branch literal>gh pr create --base <base literal> --fill --body "Sesión: <sesión> Intención: bumps patch+minor de dependencias <resumen de los commits>"Imprimí
PR URL: <url>y PARÁ ahí — el merge es del operador o de$merge-queue, nunca de esta skill. -
Resultado final esperado:
- 1 a 3 commits nuevos en TU rama de sesión (worktree),
chore/<DDMMYYYY>-vuln-audit. - Working tree limpio.
audit-report.mdactualizado.- Rama pusheada + PR abierto (arriba); sin merge.
- 1 a 3 commits nuevos en TU rama de sesión (worktree),
Modo --upgrade — reporte final
Se escribe UNA vez al cierre del loop (## Modo --upgrade › Cierre), en español, con el
estilo de casa de los audit-report.md del fleet; sobrescribe el archivo existente en la
raíz del proyecto:
# Auditoría de vulnerabilidades y dependencias — modernización secuencial
**Fecha:** <YYYY-MM-DD>
**Rama:** chore/<DDMMYYYY>-deps-upgrade
**Base:** <base> @ <sha7>
**Alcance:** patch + minor + majors, un commit por unidad con CI verde por commit · superficies: <ambas | frontend | backend>
## Resultado ejecutivo
| Métrica | Antes | Después |
|---|---|---|
| npm audit (C/H/M/L) | … | … |
| npm outdated (directas) | … | … |
| pip-audit (vulns / paquetes) | … | … |
| pip outdated (directas) | … | … |
| Unidades aplicadas / vacías / diferidas | — | A / V / D |
## Actualizaciones aplicadas
### Frontend
| Paquete directo | Antes | Después | Unidad | Commit |
|---|---|---|---|---|
### Backend
| Paquete directo | Antes | Después | Unidad | Commit |
|---|---|---|---|---|
## Rollback y excepciones
(pin-backs dentro de batches, ERESOLVE, `pip check`, unidades vacías, smoke local rojo)
## Actualizaciones mayores diferidas
| Paquete | Actual | Objetivo | Causa | Evidencia (run / log) | Commit de revert |
|---|---|---|---|---|---|
## Verificaciones ejecutadas
| Verificación | Resultado |
|---|---|
(smoke local por unidad + run de CI por commit, con ids)
## Evidencia temporal de la ejecución
(ledger completo: unidad · commit · run · veredicto · iteraciones · hora; snapshots en `/tmp/<proyecto>-*.json`)
## Acción operativa posterior al merge
- El deploy reinstala (`pip install -r backend/requirements.txt`, `npm ci && npm run build`); sin migraciones nuevas (`makemigrations --check` limpio en cada major de Django).
- Paquetes runtime-only que el CI no ejercita (`CI: no cubre`): <lista> — validar con `$deploy-and-check` tras el deploy.
- Majors diferidos: orden sugerido para la próxima corrida.
Commit docs: dependency upgrade report (<YYYY-MM-DD>) → git push → esperar su CI (el DoD
de sesión es CI verde en HEAD) → PR URL: <url> y PARÁ.
Idempotencia
- Corrida default: siempre re-escanea y re-muestra el plan; como no escribe nada, repetirla es gratis.
- En
--apply, si no hay vulns ni outdated relevantes:- No hacer commits de deps.
- Generar igual el
audit-report.mdindicando "No updates applicable" en cada sección. - Hacer el commit del reporte solo si su contenido cambió respecto al existente.
--upgradereanuda desde git, no desde memoria: (1) reutiliza worktree/rama/PRdeps-upgradesi el PR sigueOPEN(o no existe aún); (2)git log --format='%h %s' origin/<base literal>..HEADreconstruye el ledger —chore(deps):= aplicada,revert(deps):= diferida,fix(deps):= iteración; (3) re-escanea outdated en HEAD → las unidades restantes; (4) los paquetes conrevert(deps):en el log, los listados en## Actualizaciones mayores diferidasdelaudit-report.mdcommiteado y los de/tmp/<proyecto>-deps-upgrade-deferred.txtse saltan (para reintentar uno, el operador borra su fila y lo pide); (5) si el CI de HEAD está pending/rojo al reanudar, primero se resuelve ese veredicto (Pasos 6–7); (6) tree sucio limitado a archivos de deps (unidad a medias) ⇒ ⏸️ con las dos salidas (git checkout -- <archivos literales>o commitear) — nunca se descarta solo.
Ejemplos de invocación
$vuln-audit— auditar backend + frontend y mostrar el plan (read-only).$vuln-audit frontend— solo npm, solo plan.$vuln-audit backend— solo pip, solo plan.$vuln-audit --apply— aplicar patch+minor del plan en ambas superficies (commits separados, PR al final).$vuln-audit backend --apply— aplicar solo en Python.$vuln-audit --upgrade— modernización secuencial de ambas superficies (un PR, un commit por unidad, CI verde por commit).$vuln-audit frontend --upgrade— sólo npm; una corrida posteriorbackend --upgradereutiliza el mismo PR.
Acciones disponibles
Tras el reporte del plan (corrida default, sin flags), si la sesión es interactiva y NO hubo flags explícitos (reglas de gating de $output-protocol §4), ofrecer vía AskUserQuestion:
| Opción (label) | description (costo/efecto) | preview (comando exacto) |
|---|---|---|
| Aplicar patch+minor del plan (commits separados) (Recommended) | aplica los bumps del plan respetando pins, verifica (build / check / collect-only) y deja 1–3 commits en tu worktree + PR abierto, sin merge | $vuln-audit --apply |
Modernizar todo secuencialmente (--upgrade) |
patch → minor → cada major en su commit, un push por commit y CI verde antes de seguir; majors rojos revertidos y diferidos — horas, N runs de CI, PR abierto sin merge | $vuln-audit --upgrade |
| Sólo backend / sólo frontend | re-corre la auditoría acotada a una superficie | $vuln-audit backend · $vuln-audit frontend |
| Evaluar los majors saltados (plan detallado por paquete) | analiza breaking changes, esfuerzo y orden sugerido de cada major — no aplica nada | análisis |
*Truncated - read the full file at https://github.com/carlos18bp/tuhuella_project/blob/b0e983c11201d75fccccc02608c6bc2c12c5527b/.agents/skills/vuln-audit/