navori 0.2.20 → 0.2.22
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.
- package/README.md +32 -1
- package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +16 -2
- package/dist/assets/core/core-assets/agents/implementer.md +19 -0
- package/dist/assets/core/core-assets/agents/reviewer.md +5 -2
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +50 -5
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +81 -24
- package/dist/assets/core/core-assets/managed/idioma-rol.md +3 -0
- package/dist/assets/core/core-assets/presets/vite-react-ts/managed/stack.md +1 -1
- package/dist/assets/core/core-assets/skills/pr-create.md +3 -1
- package/dist/assets/core/core-assets/skills/review-diff.md +19 -4
- package/dist/assets/plugins/cognitive/scripts/check-cognitive.sh +64 -7
- package/dist/assets/plugins/engram/managed/engram-protocol.md +1 -0
- package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +64 -7
- package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +64 -7
- package/dist/index.js +277 -266
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,8 @@ Y genera:
|
|
|
47
47
|
| `add <plugin>` | Activa un plugin y opcionalmente instala la tool externa |
|
|
48
48
|
| `configure <section>` | Ajusta una sección del config sin re-correr el wizard |
|
|
49
49
|
| `update` | Re-detecta el repo, refresca config y corre sync en un paso |
|
|
50
|
-
| `render` | Genera CLAUDE.md y `.claude/` desde el config (preview por default; `--apply` escribe) |
|
|
50
|
+
| `render` | Genera CLAUDE.md y `.claude/` desde el config (preview por default; `--apply` escribe). `--all` renderea todos los repos del registro global; `--prune` limpia los que ya no existen |
|
|
51
|
+
| `registry <sub>` | Registro global de tus repos con navori, para `render --all` (`ls`, `scan <dir>`, `add`, `remove`, `prune`) |
|
|
51
52
|
| `sync` | Refresca los managed blocks con conflict resolution + backups |
|
|
52
53
|
| `preset init <id>` | Scaffoldea un preset local en `.navori/presets/<id>/` |
|
|
53
54
|
| `scan` | Detecta workspaces nuevos en monorepos (`pnpm-workspace.yaml` / `package.json#workspaces`) |
|
|
@@ -161,6 +162,36 @@ navori workspace render bonum --apply # escribe en cada repo
|
|
|
161
162
|
|
|
162
163
|
Storage: `~/.navori/workspaces/<name>/` (manifest + tickets/ + backups/).
|
|
163
164
|
|
|
165
|
+
## Rollout global tras un bump de navori
|
|
166
|
+
|
|
167
|
+
Cuando actualizas el CLI (`npm i -g navori@latest`), el registro global mete los
|
|
168
|
+
cambios a **todos** tus repos en un comando — sin ir uno por uno:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
navori render --all # preview: qué cambiaría en cada repo del registro
|
|
172
|
+
navori render --all --apply # escribe el render nuevo en todos
|
|
173
|
+
navori render --all --verbose # además lista cada bloque managed que cambió, por repo
|
|
174
|
+
navori render --all --prune # además limpia repos que ya no existen
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
El output es un registro autoexplicativo: header con el registro y el modo
|
|
178
|
+
(preview/apply), una línea por repo (`created`/`updated`/`conflict`/`removed`/`unchanged`),
|
|
179
|
+
un aviso que **nombra** los repos con bloques editados a mano (conflict, que el
|
|
180
|
+
render no pisa) y un roll-up `ok · changed · conflict · failed`.
|
|
181
|
+
|
|
182
|
+
El registro (`~/.navori/registry.json`) se puebla solo: cada `navori init` /
|
|
183
|
+
`navori update` te da de alta. Para arrancar con lo que ya tenías instalado,
|
|
184
|
+
escanéalo una vez:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
navori registry scan ~/dev ~/otra-carpeta # registra todo lo que tenga navori.config.json
|
|
188
|
+
navori registry ls # ver el registro (✓ presente / ✗ missing)
|
|
189
|
+
navori registry prune # quitar los que ya no existen
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Es ortogonal a los workspaces: el registro es "qué repos existen"; el workspace
|
|
193
|
+
es el perfil de policy (branchBase/prTarget) que cada repo hereda.
|
|
194
|
+
|
|
164
195
|
## Managed blocks con versionado
|
|
165
196
|
|
|
166
197
|
Cada bloque que `navori` inyecta en tu `CLAUDE.md` lleva metadata:
|
|
@@ -38,9 +38,23 @@ git diff origin/{{prTarget}}...HEAD --stat # scope REAL del PR (contr
|
|
|
38
38
|
gh auth status # gh autenticado
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Si el harness está activo,
|
|
41
|
+
Si el harness está activo, identifica el review de ESTA feature: `.claude/progress/review_<feature>.md`, con `<feature>` el id que recibiste en tu brief. Un glob amplio (`review_*.md`) sobre todos los reviews no es válido — no alcanza con que exista algún review con `APPROVED` en el directorio, tiene que ser el de este feature.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Abre ese archivo puntual y confirma que su veredicto es `APPROVED` y que su sección de scope/feature nombra la misma feature que vas a commitear. Si el review lista los archivos que revisó, compáralos contra `git diff --name-only`: si hay archivos tocados que NO aparecen en esa lista, el review no cubre el cambio completo → NO cuenta como aprobado. Aborta, no crees el PR, y devuelve al reviewer para que cubra los archivos faltantes. No basta con mencionar la diferencia y seguir.
|
|
44
|
+
|
|
45
|
+
<!-- Mantén esta regla de cobertura de archivos en sync con `skills/pr-create.md` (mismo chequeo, misma semántica de abort). -->
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
Archivo ausente, ambiguo (más de un candidato) o con veredicto/scope que no matchea la feature actual → NO cuenta como aprobado: abort, dile al usuario que falta review y nunca asumas un `APPROVED` genérico.
|
|
49
|
+
|
|
50
|
+
### Gate: no correr de más
|
|
51
|
+
|
|
52
|
+
Tu `git commit`/`push` dispara los hooks `PreToolUse`, que corren **mecánicamente**: `quality-gate-pre-commit` (re-corre `{{qualityGate.fast}}` y bloquea si está en rojo) + jscpd/semgrep/cognitive (duplicación/seguridad/complejidad). Ese es el enforcement que no se puede saltar. Además, el `reviewer` ya corrió `{{qualityGate.fast}}` verde sobre este mismo diff (evidencia en `review_<feature>.md`, este ciclo) y tú **no editas código**.
|
|
53
|
+
|
|
54
|
+
- ✅ **No corras `{{qualityGate.fast}}` a mano en el pre-flight.** Lo correrías dos veces sobre código ya verificado verde (tu corrida + el hook del commit). Confía en la evidencia del review para proceder; el hook del commit es el backstop mecánico.
|
|
55
|
+
- ▶️ **Córrelo a mano antes de commitear** solo si dudas de que pase: el diff cambió desde el review, hubo rebase/merge, o no hay evidencia fresca del gate verde. Así evitas un commit bloqueado por el hook y el reintento.
|
|
56
|
+
|
|
57
|
+
Nunca abras el PR con el gate en rojo.
|
|
44
58
|
|
|
45
59
|
## Flujo de commit (si hay cambios sin commitear)
|
|
46
60
|
|
|
@@ -48,6 +48,25 @@ Ejecutas **una sola** tarea desde inicio hasta verificación. No orquestas, no l
|
|
|
48
48
|
- **JSDoc** obligatorio en exports públicos y funciones >15 líneas o con lógica condicional densa.
|
|
49
49
|
- **Trazabilidad SDD** (solo si la feature tiene `{{sdd.specsDir}}/<feature>/tasks.md`, ver bloque SDD en `CLAUDE.md`): cada `R<n>` de tu lote queda cubierto por ≥1 test, y cada test referencia sus requisitos con un comentario `// Covers: R<n>` arriba del caso. Sin trazabilidad completa el `reviewer` rechaza.
|
|
50
50
|
- Si una herramienta falla raro (ej. tsc rompe sin diff aparente), **no improvises workaround**: anota `Estado: BLOCKED` + el motivo en `.claude/progress/impl_<feature>.md` y paras.
|
|
51
|
+
- **Mientras iteras, corre solo los tests del área que tocas** (filtro por path del runner). El gate completo del paso 4 corre al final, no en cada iteración — ahorra tiempo y contexto.
|
|
52
|
+
- **Reporters silenciosos en corridas intermedias.** El output verboso infla tu contexto; deja el verbose solo para diagnosticar un fallo concreto.
|
|
53
|
+
|
|
54
|
+
## Restraint (YAGNI)
|
|
55
|
+
|
|
56
|
+
Antes de escribir código, recorre la escalera y para en el primer peldaño que aguante:
|
|
57
|
+
|
|
58
|
+
1. **¿Necesita existir?** Necesidad especulativa → omítelo y dilo en una línea.
|
|
59
|
+
2. **¿Lo cubre la stdlib** del lenguaje? Úsala.
|
|
60
|
+
3. **¿Hay feature nativa de la plataforma?** (CSS sobre JS, `<input type="date">` sobre lib, constraint de DB sobre código de app).
|
|
61
|
+
4. **¿Lo resuelve una dependencia ya instalada?** Úsala; no agregues una nueva por lo que unas líneas hacen.
|
|
62
|
+
5. **¿Entra en una línea?** Una línea.
|
|
63
|
+
6. **Solo entonces:** el mínimo código que funciona.
|
|
64
|
+
|
|
65
|
+
Sin abstracciones especulativas: nada de interface / capa / flag con un solo caller "por si acaso". Gana el diff más corto; borrar antes que agregar. Marca cada atajo deliberado con un comentario que nombre su **techo** y su **disparador de upgrade** — ej. `// TODO(perf): lock global; shardear por cuenta si supera ~100 rps`. Un atajo sin disparador es deuda muda; el `reviewer` lo marca.
|
|
66
|
+
|
|
67
|
+
**YAGNI ≠ código incompleto ni de menor calidad.** Aplica al *alcance especulativo* (construir para un futuro hipotético), NO a la *completitud del requisito actual*: los edge cases, estados de error y validaciones de lo que SÍ estás construyendo son parte del trabajo, no "código de más". La escalera elige la solución más simple que **cubre el caso**, nunca la que cubre menos casos. **Nunca** simplifiques (siempre va): validación de input en trust boundaries, manejo de errores que evita pérdida de datos, seguridad, accesibilidad, ni nada pedido explícitamente. La lógica no trivial deja al menos UN check ejecutable.
|
|
68
|
+
|
|
69
|
+
**No deliberes de más.** Si el *alcance* es ambiguo entre mínimo y completo, entrega el mínimo razonable y cuestiónalo en la misma respuesta ("hice X; cubre Y. ¿Necesitas Z? dilo") en vez de gastar razonamiento sin escribir. Aplica al alcance, no a la calidad: la versión mínima igual va **completa** para lo que cubre.
|
|
51
70
|
|
|
52
71
|
## Evidence-based completion (gate antes del informe)
|
|
53
72
|
|
|
@@ -28,7 +28,8 @@ Eres un revisor estricto. Tu única función es **aprobar o rechazar**. No edita
|
|
|
28
28
|
git diff origin/{{prTarget}}...HEAD
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
3.
|
|
31
|
+
3. **Re-review** (si ya hay un `.claude/progress/review_<feature>.md` de un ciclo previo): enfoca la *lectura* en (a) que los issues listados ahí estén resueltos y (b) los archivos que el `implementer` reporta haber tocado en este ciclo (`impl_<feature>.md`). No re-revises desde cero el código ya aprobado que no cambió; el quality gate completo sí se corre igual — un cambio puede romper algo fuera del delta.
|
|
32
|
+
4. Aplica `.claude/skills/verify-before-done.md` para todo `[x]` que dependa de evidencia. El quality gate se corre **en este turno, en la Pasada 2** (no antes: un `SPEC_MISS` en Pasada 1 no lo necesita — no gastes el gate en un diff que vas a rechazar por spec). No asumas del cache del informe del implementer.
|
|
32
33
|
|
|
33
34
|
### Pasada 1 — Spec compliance
|
|
34
35
|
|
|
@@ -50,7 +51,7 @@ Eres un revisor estricto. Tu única función es **aprobar o rechazar**. No edita
|
|
|
50
51
|
|
|
51
52
|
¿El código matchea las convenciones del repo? Aquí sí revisas estilo/naming/tipos.
|
|
52
53
|
|
|
53
|
-
Aplica `.claude/skills/review-diff.md` — la checklist completa por dimensiones (tipos, capa de datos, errores, seguridad, hardcode, naming, dead code) con severidades. Sus CRÍTICO/ALTO mapean a los issues ≥80 de abajo; MEDIO a las observaciones informativas. Resumen de lo mínimo a validar contra `CLAUDE.md` y las "Reglas del proyecto" del leader:
|
|
54
|
+
Aplica `.claude/skills/review-diff.md` — la checklist completa por dimensiones (tipos, capa de datos, errores, seguridad, hardcode, naming, sobre-ingeniería, dead code) con severidades. Sus CRÍTICO/ALTO mapean a los issues ≥80 de abajo; MEDIO a las observaciones informativas. Resumen de lo mínimo a validar contra `CLAUDE.md` y las "Reglas del proyecto" del leader:
|
|
54
55
|
|
|
55
56
|
- **Convenciones**: naming, path aliases, estructura de carpetas.
|
|
56
57
|
- **Tipos centralizados**: no `type`/`interface` inline donde la convención dice "afuera".
|
|
@@ -66,6 +67,8 @@ Aplica `.claude/skills/review-diff.md` — la checklist completa por dimensiones
|
|
|
66
67
|
{{qualityGate.fast}}
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
Léelo completo para verificar (exit code + conteo de failures), pero al informe deja solo `exit 0` + la línea de resumen (ej. `N passed`); en rojo, solo el tail que falla. No arrastres el log verboso completo turno a turno. Esta evidencia —gate verde sobre el diff final, en este ciclo— es la que el `commit-pr-pilot` reusa para **no** re-correr el gate, así que debe ser fresca y sobre el diff que se va a commitear.
|
|
71
|
+
|
|
69
72
|
Si el informe del implementer dice "UI no validada" y el cambio toca pantallas, márcalo para verificación humana — no apruebes solo.
|
|
70
73
|
|
|
71
74
|
**Veredicto parcial:**
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
# devices. Exit 2 in a PreToolUse hook is evaluated BEFORE permission rules, so
|
|
9
9
|
# this is the strongest line of defense — it overrides even an `allow` match.
|
|
10
10
|
#
|
|
11
|
+
# KNOWN, ACCEPTED LIMITATION: matching is regex-based over the command string.
|
|
12
|
+
# Multi-line continuations, git global options (`git -C … commit`), and simple
|
|
13
|
+
# wrappers (`command`/`\git`/parens) ARE normalized before matching now, and a
|
|
14
|
+
# quoted skip-flag (`git commit "--no-verify"`) is still caught. Still unhandled:
|
|
15
|
+
# `sh -c`, `eval`, base64/printf obfuscation, and escaped-quote edge cases inside
|
|
16
|
+
# quoted spans. This guard is a SEATBELT against accidental
|
|
17
|
+
# destructive commands, NOT a sandbox against a deliberate adversary.
|
|
18
|
+
#
|
|
11
19
|
# The managed block below is regenerated by `navori render` and must NOT be
|
|
12
20
|
# edited by hand. Add your repo-specific guards in the user section at the
|
|
13
21
|
# bottom — `$cmd` is already parsed and in scope there.
|
|
@@ -42,17 +50,54 @@ block() {
|
|
|
42
50
|
exit 2
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
# Normalized copies used ONLY by the hook-skip (1) and force-push (2) rules
|
|
54
|
+
# below. `$cmd` stays intact for the messages and the other rules. The transforms
|
|
55
|
+
# only add boundaries / drop noise, so they always push toward MORE blocking:
|
|
56
|
+
# FIX B — join `\<newline>` continuations into a space and flatten any other
|
|
57
|
+
# newline to `;` (a boundary the rules already handle) so a multi-line
|
|
58
|
+
# `git commit \<NL> --no-verify` is matched as one logical command.
|
|
59
|
+
# FIX C — neutralize wrappers into boundaries: a leading `command ` word, a
|
|
60
|
+
# leading backslash (`\git`), and `(` runs, so `command git …`,
|
|
61
|
+
# `\git …` and `(git …)` read as a plain `git …`.
|
|
62
|
+
# `scan` keeps quoted spans intact, so a quoted skip-flag (`git commit
|
|
63
|
+
# "--no-verify"`) is still caught — stripping the quotes there was a real bypass.
|
|
64
|
+
# `scan_flags` additionally drops quoted spans and is used ONLY by the
|
|
65
|
+
# combined-short-flag check, so a hyphen-word inside a commit message
|
|
66
|
+
# (`-m "add -notify option"`) can't trip it.
|
|
67
|
+
scan="$cmd"
|
|
68
|
+
scan="${scan//\\$'\n'/ }" # FIX B: join line continuations
|
|
69
|
+
scan="${scan//$'\n'/;}" # FIX B: flatten remaining newlines to a boundary
|
|
70
|
+
scan=$(printf '%s' "$scan" | sed -E \
|
|
71
|
+
-e "s/(^|[;&|]|[[:space:]])command[[:space:]]+/\1/g" \
|
|
72
|
+
-e "s/\\\\([A-Za-z])/ \1/g" \
|
|
73
|
+
-e "s/\(/ /g")
|
|
74
|
+
scan_flags=$(printf '%s' "$scan" | sed -E -e "s/'[^']*'//g" -e "s/\"[^\"]*\"//g")
|
|
75
|
+
|
|
76
|
+
# Git commit/push detector shared by rule 1. The leading boundary accepts ; & |
|
|
77
|
+
# so `true;git commit …` (no space after the separator) is caught; the
|
|
78
|
+
# `git(… -opt …)*` middle allows git global options between `git` and the
|
|
79
|
+
# subcommand (`git -c k=v commit …`).
|
|
80
|
+
git_cp='(^|[[:space:]]|[;&|])git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+(commit|push)'
|
|
81
|
+
|
|
45
82
|
# 1. Skipping quality gates / hooks — defeats the whole point of the harness.
|
|
46
|
-
|
|
83
|
+
# Two checks: the literal `--no-verify` over `scan` (quotes kept, so a quoted
|
|
84
|
+
# flag is still caught), and `-[a-zA-Z]*n[a-zA-Z]*` over `scan_flags` (quotes
|
|
85
|
+
# dropped) to catch `-n` folded into a combined short-flag token
|
|
86
|
+
# (`git commit -qn`/`-nq`) without a hyphen-word in a message tripping it.
|
|
87
|
+
if printf '%s' "$scan" | grep -qE "${git_cp}([[:space:]]|.)*--no-verify" \
|
|
88
|
+
|| printf '%s' "$scan_flags" | grep -qE "${git_cp}([[:space:]]|.)*[[:space:]]-[a-zA-Z]*n[a-zA-Z]*([[:space:]]|\$)"; then
|
|
47
89
|
block "git commit/push con --no-verify (saltarse los hooks/gates)"
|
|
48
90
|
fi
|
|
49
91
|
|
|
50
92
|
# 2. Force-push to the base branch. force-with-lease is allowed (safe rebase
|
|
51
93
|
# flow on feature branches); bare --force/-f against the base branch is not.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
94
|
+
# The leading boundary accepts ; & | so `true;git push --force main` (no
|
|
95
|
+
# space after the separator) is caught too. The `git(… -opt …)*` middle
|
|
96
|
+
# allows git global options before `push` (`git -C /repo push --force …`).
|
|
97
|
+
if printf '%s' "$scan" | grep -qE '(^|[[:space:]]|[;&|])git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+push' \
|
|
98
|
+
&& printf '%s' "$scan" | grep -qE '(--force([[:space:]]|$)|[[:space:]]-f([[:space:]]|$)|[[:space:]]\+)' \
|
|
99
|
+
&& ! printf '%s' "$scan" | grep -qE 'force-with-lease' \
|
|
100
|
+
&& printf '%s' "$scan" | grep -qE "(^|[[:space:]+/])${base}([[:space:]]|\$)"; then
|
|
56
101
|
block "force-push a la rama base '${base}'"
|
|
57
102
|
fi
|
|
58
103
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Pre-commit / pre-push quality gate hook.
|
|
4
4
|
# Reads the tool input (from Claude Code's PreToolUse Bash matcher) and runs
|
|
5
|
-
# the project's `qualityGate.fast` command before allowing the commit
|
|
5
|
+
# the project's `qualityGate.fast` command before allowing the commit.
|
|
6
6
|
#
|
|
7
7
|
# The managed block below is regenerated by `navori render` and must NOT be
|
|
8
8
|
# edited by hand. Add your repo-specific checks in the user section at the
|
|
@@ -51,35 +51,92 @@ is_pm() {
|
|
|
51
51
|
run_gate() {
|
|
52
52
|
echo "[navori] running quality-gate fast: $1" >&2
|
|
53
53
|
eval "$1" || {
|
|
54
|
-
echo "[navori] quality-gate fast failed. Commit
|
|
54
|
+
echo "[navori] quality-gate fast failed. Commit aborted." >&2
|
|
55
55
|
exit 2
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
# --- shared gate detection (keep IN SYNC across sibling hooks) --------------
|
|
60
|
+
# Detect a `git commit` invocation anywhere in a (possibly
|
|
61
|
+
# compound) command. Splits $1 on the shell separators && || ; | and newlines,
|
|
62
|
+
# strips leading whitespace plus simple `VAR=value` env prefixes from each
|
|
63
|
+
# segment, and returns 0 if ANY segment STARTS with `git commit` on
|
|
64
|
+
# a word boundary. Replaces literal-prefix `case` matching, which silently
|
|
65
|
+
# skipped the gate for `cd x && git commit`, `echo y; git commit`, or a leading
|
|
66
|
+
# space (#88: NEVER skip the gate silently). Because it matches a segment START,
|
|
67
|
+
# a quoted `echo "git commit"` does NOT trigger it. Known limitation: it cannot
|
|
68
|
+
# see through `sh -c`, `eval`, or obfuscation — a seatbelt, not a sandbox.
|
|
69
|
+
# The IDENTICAL function body lives in the sibling gate scripts (they render
|
|
70
|
+
# standalone, so there is no shared lib to import):
|
|
71
|
+
# plugins/jscpd/scripts/check-jscpd.sh
|
|
72
|
+
# plugins/semgrep/scripts/check-semgrep.sh
|
|
73
|
+
# plugins/cognitive/scripts/check-cognitive.sh
|
|
74
|
+
is_git_commit() {
|
|
75
|
+
local input="$1" segment
|
|
76
|
+
# FIX B: join `\<newline>` continuations into a space FIRST, so a command
|
|
77
|
+
# split across lines with a trailing backslash stays ONE logical segment
|
|
78
|
+
# (otherwise the subcommand/flag lands in a segment not starting with git).
|
|
79
|
+
input="${input//\\$'\n'/ }"
|
|
80
|
+
input="${input//&&/$'\n'}"
|
|
81
|
+
input="${input//||/$'\n'}"
|
|
82
|
+
input="${input//;/$'\n'}"
|
|
83
|
+
input="${input//|/$'\n'}"
|
|
84
|
+
# `<<<` feeds the already-expanded value as data — no re-evaluation — so a
|
|
85
|
+
# command that contains backticks/$() is inspected, never executed.
|
|
86
|
+
while IFS= read -r segment; do
|
|
87
|
+
segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
|
|
88
|
+
# FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
|
|
89
|
+
# `VAR=val git …` all reduce to a plain `git …` before matching.
|
|
90
|
+
while [[ "$segment" == \(* ]]; do # strip leading ( runs
|
|
91
|
+
segment="${segment#\(}"
|
|
92
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
93
|
+
done
|
|
94
|
+
segment="${segment#\\}" # strip a leading backslash (\git)
|
|
95
|
+
while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
|
|
96
|
+
case "$segment" in
|
|
97
|
+
*[[:space:]]*)
|
|
98
|
+
segment="${segment#*[[:space:]]}"
|
|
99
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
100
|
+
;;
|
|
101
|
+
*) segment=""; break ;;
|
|
102
|
+
esac
|
|
103
|
+
done
|
|
104
|
+
if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
|
|
105
|
+
segment="${segment#command }"
|
|
106
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
107
|
+
fi
|
|
108
|
+
# FIX C: allow git global options between `git` and the subcommand
|
|
109
|
+
# (`git -c k=v commit`, `git -C /repo push`). Trailing boundary keeps
|
|
110
|
+
# `git commitgraph` / `git config …` from matching as commit.
|
|
111
|
+
if printf '%s' "$segment" | grep -qE '^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'; then
|
|
112
|
+
return 0
|
|
113
|
+
fi
|
|
114
|
+
done <<< "$input"
|
|
115
|
+
return 1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if is_git_commit "$cmd"; then
|
|
119
|
+
gate="{{qualityGate.fast}}"
|
|
120
|
+
gate_bin="${gate%% *}"
|
|
121
|
+
if command -v "$gate_bin" >/dev/null 2>&1; then
|
|
122
|
+
run_gate "$gate"
|
|
123
|
+
else
|
|
124
|
+
# The declared runner isn't on PATH. If it's a package manager, detect the
|
|
125
|
+
# repo's real one from lockfiles and retry through it (a `pnpm run x` gate
|
|
126
|
+
# still runs in a bun-only checkout). #88: NEVER skip the gate silently —
|
|
127
|
+
# when nothing can run it, BLOCK the commit loudly instead of the old
|
|
128
|
+
# `exit 0` that handed a contributor zero quality gate without a word.
|
|
129
|
+
detected_pm="$(detect_pm)"
|
|
130
|
+
if is_pm "$gate_bin" && [ -n "$detected_pm" ] && [ "$detected_pm" != "$gate_bin" ] && command -v "$detected_pm" >/dev/null 2>&1; then
|
|
131
|
+
echo "[navori] '$gate_bin' no está en PATH; uso el package manager detectado por lockfile: '$detected_pm'." >&2
|
|
132
|
+
run_gate "$detected_pm ${gate#* }"
|
|
65
133
|
else
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# when nothing can run it, BLOCK the commit loudly instead of the old
|
|
70
|
-
# `exit 0` that handed a contributor zero quality gate without a word.
|
|
71
|
-
detected_pm="$(detect_pm)"
|
|
72
|
-
if is_pm "$gate_bin" && [ -n "$detected_pm" ] && [ "$detected_pm" != "$gate_bin" ] && command -v "$detected_pm" >/dev/null 2>&1; then
|
|
73
|
-
echo "[navori] '$gate_bin' no está en PATH; uso el package manager detectado por lockfile: '$detected_pm'." >&2
|
|
74
|
-
run_gate "$detected_pm ${gate#* }"
|
|
75
|
-
else
|
|
76
|
-
echo "[navori] quality-gate NO ejecutado: '$gate_bin' no está en PATH y no hay un package manager alternativo detectado que pueda correrlo." >&2
|
|
77
|
-
echo "[navori] Commit/push BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin' o usa 'git commit --no-verify' si de verdad quieres saltártelo." >&2
|
|
78
|
-
exit 2
|
|
79
|
-
fi
|
|
134
|
+
echo "[navori] quality-gate NO ejecutado: '$gate_bin' no está en PATH y no hay un package manager alternativo detectado que pueda correrlo." >&2
|
|
135
|
+
echo "[navori] Commit BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin' o usa 'git commit --no-verify' si de verdad quieres saltártelo." >&2
|
|
136
|
+
exit 2
|
|
80
137
|
fi
|
|
81
|
-
|
|
82
|
-
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
83
140
|
|
|
84
141
|
# navori:user-section
|
|
85
142
|
# user: agrega checks adicionales acá. `$cmd` ya está parseado del input
|
|
@@ -2,3 +2,6 @@
|
|
|
2
2
|
|
|
3
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.
|
|
5
|
+
- **Alcance de persona**: idioma y tono de esta sección rigen solo la respuesta directa al usuario (chat). No rigen artefactos generados (código, identificadores, comentarios, commits, título/descripción de PR, docs).
|
|
6
|
+
- Default de artefactos: código e identificadores en inglés. Copy de UI, PRs y docs siguen el idioma configurado del proyecto (`language` en `navori.config.json`), no el idioma del chat.
|
|
7
|
+
- Nunca inyectes tono o énfasis de persona (mayúsculas, exclamaciones, coloquialismos) en artefactos — eso es exclusivo del chat.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Stack — Vite + React + TypeScript
|
|
2
2
|
|
|
3
|
-
SPA sobre Vite + React
|
|
3
|
+
SPA sobre Vite + React + TypeScript, agnóstica de UI lib (CSS Modules, Tailwind, styled-components, o una lib de componentes). Organización por feature: cada feature vive en su carpeta con sus componentes, hooks y estado local; lo compartido (UI primitives, utils, hooks genéricos) va a carpetas comunes. Los componentes son funcionales con hooks; nada de class components nuevos.
|
|
4
4
|
|
|
5
5
|
Regla de oro: tipado estricto (sin `any` injustificado — ver el bloque de tipado); side-effects en `useEffect` con deps completas; data-fetching por la capa que use el repo (fetch/axios, TanStack Query si está — se inyecta como library-skill según deps). El estado del servidor NO se duplica en estado global; el estado global (Redux/Zustand/Context) es solo para lo genuinamente compartido y de cliente. Aplica la skill `new-feature` para dar de alta una feature nueva con la estructura del repo. Las skills de UI lib, forms y state se inyectan según las dependencias que detecte navori.
|
|
6
6
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
name: pr-create
|
|
3
3
|
description: Crea un PR en GitHub contra la rama target del repo, delegando title/body a un modelo y validando el output. Antes de crear cualquier PR al cerrar el ciclo de un ticket (Fase 8 de ticket-intake).
|
|
4
4
|
type: reference
|
|
5
|
+
# maxWords raised above the 500 reference cap: el pre-flight bloqueante es load-bearing y no se puede recortar sin perder chequeos de seguridad.
|
|
6
|
+
maxWords: 560
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
# pr-create — crear PR contra `{{prTarget}}`
|
|
@@ -18,7 +20,7 @@ Al cierre del ciclo, tras el `APPROVED` del reviewer (Fase 8 de `ticket-intake`)
|
|
|
18
20
|
2. **No estás en una rama protegida**: aborta si la rama actual es `main|master|{{branchBase}}|{{prTarget}}`.
|
|
19
21
|
3. **`gh` autenticado**: `gh auth status`.
|
|
20
22
|
4. **Gate verde en este turno**: `{{qualityGate.fast}}`. Falla → manda al implementer.
|
|
21
|
-
5. **Review APPROVED** (
|
|
23
|
+
5. **Review APPROVED de esta feature** (harness activo): `.claude/progress/review_<feature>.md` puntual — glob `review_*.md` no vale. Ausente/ambiguo/scope distinto → NO aprobado, manda al reviewer. Si lista archivos, compáralos con `git diff --name-only`: archivos tocados que el review NO cubre → NO aprobado, aborta y devuelve al reviewer (no basta con mencionar la diferencia y seguir). <!-- Mantén este chequeo de cobertura en sync con `agents/commit-pr-pilot.md`. -->
|
|
22
24
|
6. **Hay commits para el PR**: `git fetch origin {{prTarget}} --quiet` y `git log origin/{{prTarget}}..HEAD --oneline`. Vacío → nada que PR-ear.
|
|
23
25
|
7. **Branch sincronizada**: `git log HEAD..origin/{{prTarget}} --oneline`. Si origin va adelante, rebase y pregunta.
|
|
24
26
|
8. **Arrastre de commits** (solo si `{{branchBase}}` ≠ `{{prTarget}}`): `git rev-list --count origin/{{prTarget}}..origin/{{branchBase}}`. Si es > 0, `{{branchBase}}` va adelantado de `{{prTarget}}` y el PR arrastra esos commits ajenos: avisa y sugiere `git rebase origin/{{prTarget}}` antes de abrir.
|
|
@@ -49,6 +49,7 @@ Si no llega a MEDIO, no lo reportes. Nada de "nitpick" ni "consider also". (Mape
|
|
|
49
49
|
- `catch` que se traga el error sin propagar ni reportar → ALTO.
|
|
50
50
|
- Operación que puede fallar (red, parse, IO) sin manejo, con el fallo visible al usuario → ALTO.
|
|
51
51
|
- Loading/spinner que nunca se apaga en el path de error → ALTO.
|
|
52
|
+
- Recurso abierto sin cleanup en el path de error (conexión, lock, stream, listener, subscription, timer): el happy path lo libera pero un `catch`/early-return lo filtra → ALTO.
|
|
52
53
|
|
|
53
54
|
## 4. Seguridad y autorización
|
|
54
55
|
|
|
@@ -68,18 +69,32 @@ Si no llega a MEDIO, no lo reportes. Nada de "nitpick" ni "consider also". (Mape
|
|
|
68
69
|
- Casing/sufijos que rompen la convención del repo → MEDIO.
|
|
69
70
|
- Convención de migración rota (cuando conviven código nuevo y legacy y hay un sufijo/carpeta esperado) → ALTO.
|
|
70
71
|
|
|
71
|
-
## 7.
|
|
72
|
+
## 7. Sobre-ingeniería / abstracción especulativa
|
|
73
|
+
|
|
74
|
+
Espejo de la escalera YAGNI del `implementer`: caza el código de **más**.
|
|
75
|
+
|
|
76
|
+
- Abstracción (interface, capa, helper genérico, hook) con **un solo caller** y sin segundo consumidor a la vista → MEDIO (ALTO si acopla o complica un área crítica).
|
|
77
|
+
- Dependencia nueva para lo que la stdlib, una feature nativa de la plataforma o una lib ya instalada resuelven en unas líneas → ALTO.
|
|
78
|
+
- Parametrización, flags de config u opciones que nadie usa todavía ("por si acaso") → MEDIO.
|
|
79
|
+
- Indirección o patrón (factory, wrapper, capa de eventos) que no elimina duplicación real ni cubre un requisito presente → MEDIO.
|
|
80
|
+
- Atajo deliberado sin su marca (techo + disparador de upgrade) → MEDIO: la deuda muda es peor que la declarada.
|
|
81
|
+
|
|
82
|
+
Regla: si quitar la abstracción deja el código **igual de correcto** y más corto, quitarla es el hallazgo.
|
|
83
|
+
|
|
84
|
+
**No confundir con incompletitud.** Quitar el manejo de un edge case real, una validación o un path de error NO es simplificar — es un bug, y va a §1-§4 (no aquí). Esta dimensión ataca *estructura de más*, nunca *cobertura de menos*. Lo que la escalera YAGNI protege (trust boundaries, errores que evitan pérdida de datos, seguridad, accesibilidad) nunca es over-engineering.
|
|
85
|
+
|
|
86
|
+
## 8. Dead code y debug
|
|
72
87
|
|
|
73
88
|
- `console.log` / print de debug sin guard en código que se mergea → MEDIO (en código nuevo: ALTO).
|
|
74
89
|
- Imports o variables sin usar → MEDIO.
|
|
75
90
|
- Código comentado entero / `if (false)` / `// TODO: borrar` sin issue → MEDIO.
|
|
76
91
|
|
|
77
|
-
##
|
|
92
|
+
## 9. Quality gate (corrido en este turno, no asumido)
|
|
78
93
|
|
|
79
94
|
- `{{qualityGate.fast}}` pasa → CRÍTICO si falla.
|
|
80
95
|
- Cero errores/warnings nuevos vs baseline → ALTO si el diff los agrega.
|
|
81
96
|
|
|
82
|
-
##
|
|
97
|
+
## 10. Commit y PR
|
|
83
98
|
|
|
84
99
|
- Commits siguen la convención del repo → MEDIO si rompe.
|
|
85
100
|
- Cambios a manifest/lockfile sin razón clara en la descripción → ALTO.
|
|
@@ -98,7 +113,7 @@ Presta atención extra si el diff toca las áreas críticas que declara tu repo
|
|
|
98
113
|
## Conexión con el harness
|
|
99
114
|
|
|
100
115
|
- `reviewer`: aplica este skill en la Pasada 2 (code quality). CRÍTICO/ALTO mapean a issues con confidence ≥80 (bloquean APPROVED); MEDIO a observaciones informativas (50-79).
|
|
101
|
-
- `verify-before-done`: el quality gate del §
|
|
116
|
+
- `verify-before-done`: el quality gate del §9 se corre en este turno, no se asume del informe del implementer.
|
|
102
117
|
|
|
103
118
|
<!-- navori:user-section -->
|
|
104
119
|
## Reglas específicas del repo
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
# it once:
|
|
10
10
|
# (cd .claude/scripts/cognitive-tool && bun install)
|
|
11
11
|
#
|
|
12
|
-
# Triggered as a PreToolUse(Bash) hook (gated to git commit
|
|
12
|
+
# Triggered as a PreToolUse(Bash) hook (gated to git commit) and as a
|
|
13
13
|
# Stop hook (runs unconditionally at session close). Skips silently when the
|
|
14
14
|
# toolchain is not bootstrapped — the gate is optional.
|
|
15
15
|
|
|
16
16
|
set -euo pipefail
|
|
17
17
|
|
|
18
|
-
# PreToolUse(Bash) passes the command — gate to commit
|
|
18
|
+
# PreToolUse(Bash) passes the command — gate to commit. The Stop hook
|
|
19
19
|
# passes no command — run unconditionally at session close. Extract without
|
|
20
20
|
# hard-depending on jq (not preinstalled on macOS): try jq, then node (Claude
|
|
21
21
|
# Code's own runtime), then a best-effort sed unwrap. No command extracted →
|
|
@@ -31,11 +31,68 @@ extract_cmd() {
|
|
|
31
31
|
printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
|
|
32
32
|
}
|
|
33
33
|
cmd=$(extract_cmd)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
|
|
35
|
+
# --- shared gate detection (keep IN SYNC across sibling hooks) --------------
|
|
36
|
+
# Detect a `git commit` invocation anywhere in a (possibly
|
|
37
|
+
# compound) command. Splits $1 on the shell separators && || ; | and newlines,
|
|
38
|
+
# strips leading whitespace plus simple `VAR=value` env prefixes from each
|
|
39
|
+
# segment, and returns 0 if ANY segment STARTS with `git commit` on
|
|
40
|
+
# a word boundary. Replaces literal-prefix `case` matching, which silently
|
|
41
|
+
# skipped the gate for `cd x && git commit`, `echo y; git commit`, or a leading
|
|
42
|
+
# space. Because it matches a segment START, a quoted `echo "git commit"` does
|
|
43
|
+
# NOT trigger it. Known limitation: it cannot see through `sh -c`, `eval`, or
|
|
44
|
+
# obfuscation — a seatbelt, not a sandbox. Body duplicated across the navori quality hooks (they render standalone — no
|
|
45
|
+
# shared lib). The quality-gate/jscpd/cognitive copies gate `git commit` ONLY;
|
|
46
|
+
# the semgrep copy also gates `git push` and `gh pr create` (remote-push
|
|
47
|
+
# security backstop). Keep in sync.
|
|
48
|
+
is_git_commit() {
|
|
49
|
+
local input="$1" segment
|
|
50
|
+
# FIX B: join `\<newline>` continuations into a space FIRST, so a command
|
|
51
|
+
# split across lines with a trailing backslash stays ONE logical segment
|
|
52
|
+
# (otherwise the subcommand/flag lands in a segment not starting with git).
|
|
53
|
+
input="${input//\\$'\n'/ }"
|
|
54
|
+
input="${input//&&/$'\n'}"
|
|
55
|
+
input="${input//||/$'\n'}"
|
|
56
|
+
input="${input//;/$'\n'}"
|
|
57
|
+
input="${input//|/$'\n'}"
|
|
58
|
+
# `<<<` feeds the already-expanded value as data — no re-evaluation — so a
|
|
59
|
+
# command that contains backticks/$() is inspected, never executed.
|
|
60
|
+
while IFS= read -r segment; do
|
|
61
|
+
segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
|
|
62
|
+
# FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
|
|
63
|
+
# `VAR=val git …` all reduce to a plain `git …` before matching.
|
|
64
|
+
while [[ "$segment" == \(* ]]; do # strip leading ( runs
|
|
65
|
+
segment="${segment#\(}"
|
|
66
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
67
|
+
done
|
|
68
|
+
segment="${segment#\\}" # strip a leading backslash (\git)
|
|
69
|
+
while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
|
|
70
|
+
case "$segment" in
|
|
71
|
+
*[[:space:]]*)
|
|
72
|
+
segment="${segment#*[[:space:]]}"
|
|
73
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
74
|
+
;;
|
|
75
|
+
*) segment=""; break ;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
|
|
79
|
+
segment="${segment#command }"
|
|
80
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
81
|
+
fi
|
|
82
|
+
# FIX C: allow git global options between `git` and the subcommand
|
|
83
|
+
# (`git -c k=v commit`, `git -C /repo push`). Trailing boundary keeps
|
|
84
|
+
# `git commitgraph` / `git config …` from matching as commit.
|
|
85
|
+
if printf '%s' "$segment" | grep -qE '^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'; then
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
done <<< "$input"
|
|
89
|
+
return 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# No command extracted (empty $cmd) → run unconditionally (Stop hook path). A
|
|
93
|
+
# real command that is NOT a git commit → skip. Anything else → scan.
|
|
94
|
+
if [ -n "$cmd" ] && ! is_git_commit "$cmd"; then
|
|
95
|
+
exit 0
|
|
39
96
|
fi
|
|
40
97
|
|
|
41
98
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
- `mem_save` proactivo tras decisión / bug-fix-con-root-cause / convención / discovery / preferencia confirmada.
|
|
4
4
|
- `mem_search` al inicio si el primer mensaje referencia el proyecto. Verificar en código que lo recordado siga existiendo antes de afirmarlo.
|
|
5
|
+
- Lo recordado es una foto de cuando se guardó, no un hecho vigente. Si el sistema expone vigencia (`active` / `needs_review`), trata `needs_review` como contexto stale: avisa al usuario y verifícalo contra el código actual antes de apoyarte en eso.
|
|
5
6
|
- `mem_session_summary` obligatorio antes de "listo": Goal · Discoveries · Accomplished · Next Steps · Relevant Files.
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
# `{{branchBase}}`. Skips silently if jscpd or git are absent — the tool is
|
|
4
4
|
# optional, not a hard dependency of the project.
|
|
5
5
|
#
|
|
6
|
-
# Triggered as a PreToolUse(Bash) hook, gated to git commit
|
|
6
|
+
# Triggered as a PreToolUse(Bash) hook, gated to git commit — so the
|
|
7
7
|
# duplication check runs right before code lands, not on every turn.
|
|
8
8
|
|
|
9
9
|
set -euo pipefail
|
|
10
10
|
|
|
11
|
-
# PreToolUse(Bash) passes the command — gate to commit
|
|
11
|
+
# PreToolUse(Bash) passes the command — gate to commit. Extract without
|
|
12
12
|
# hard-depending on jq (not preinstalled on macOS): try jq, then node (Claude
|
|
13
13
|
# Code's own runtime), then a best-effort sed unwrap. No command extracted →
|
|
14
14
|
# empty $cmd → runs unconditionally (defensive: never silently skip a commit).
|
|
@@ -23,11 +23,68 @@ extract_cmd() {
|
|
|
23
23
|
printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
|
|
24
24
|
}
|
|
25
25
|
cmd=$(extract_cmd)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
|
|
27
|
+
# --- shared gate detection (keep IN SYNC across sibling hooks) --------------
|
|
28
|
+
# Detect a `git commit` invocation anywhere in a (possibly
|
|
29
|
+
# compound) command. Splits $1 on the shell separators && || ; | and newlines,
|
|
30
|
+
# strips leading whitespace plus simple `VAR=value` env prefixes from each
|
|
31
|
+
# segment, and returns 0 if ANY segment STARTS with `git commit` on
|
|
32
|
+
# a word boundary. Replaces literal-prefix `case` matching, which silently
|
|
33
|
+
# skipped the gate for `cd x && git commit`, `echo y; git commit`, or a leading
|
|
34
|
+
# space. Because it matches a segment START, a quoted `echo "git commit"` does
|
|
35
|
+
# NOT trigger it. Known limitation: it cannot see through `sh -c`, `eval`, or
|
|
36
|
+
# obfuscation — a seatbelt, not a sandbox. Body duplicated across the navori quality hooks (they render standalone — no
|
|
37
|
+
# shared lib). The quality-gate/jscpd/cognitive copies gate `git commit` ONLY;
|
|
38
|
+
# the semgrep copy also gates `git push` and `gh pr create` (remote-push
|
|
39
|
+
# security backstop). Keep in sync.
|
|
40
|
+
is_git_commit() {
|
|
41
|
+
local input="$1" segment
|
|
42
|
+
# FIX B: join `\<newline>` continuations into a space FIRST, so a command
|
|
43
|
+
# split across lines with a trailing backslash stays ONE logical segment
|
|
44
|
+
# (otherwise the subcommand/flag lands in a segment not starting with git).
|
|
45
|
+
input="${input//\\$'\n'/ }"
|
|
46
|
+
input="${input//&&/$'\n'}"
|
|
47
|
+
input="${input//||/$'\n'}"
|
|
48
|
+
input="${input//;/$'\n'}"
|
|
49
|
+
input="${input//|/$'\n'}"
|
|
50
|
+
# `<<<` feeds the already-expanded value as data — no re-evaluation — so a
|
|
51
|
+
# command that contains backticks/$() is inspected, never executed.
|
|
52
|
+
while IFS= read -r segment; do
|
|
53
|
+
segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
|
|
54
|
+
# FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
|
|
55
|
+
# `VAR=val git …` all reduce to a plain `git …` before matching.
|
|
56
|
+
while [[ "$segment" == \(* ]]; do # strip leading ( runs
|
|
57
|
+
segment="${segment#\(}"
|
|
58
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
59
|
+
done
|
|
60
|
+
segment="${segment#\\}" # strip a leading backslash (\git)
|
|
61
|
+
while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
|
|
62
|
+
case "$segment" in
|
|
63
|
+
*[[:space:]]*)
|
|
64
|
+
segment="${segment#*[[:space:]]}"
|
|
65
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
66
|
+
;;
|
|
67
|
+
*) segment=""; break ;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
|
|
71
|
+
segment="${segment#command }"
|
|
72
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
73
|
+
fi
|
|
74
|
+
# FIX C: allow git global options between `git` and the subcommand
|
|
75
|
+
# (`git -c k=v commit`, `git -C /repo push`). Trailing boundary keeps
|
|
76
|
+
# `git commitgraph` / `git config …` from matching as commit.
|
|
77
|
+
if printf '%s' "$segment" | grep -qE '^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'; then
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
done <<< "$input"
|
|
81
|
+
return 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# No command extracted (empty $cmd) → run unconditionally. A real command that
|
|
85
|
+
# is NOT a git commit → skip. Anything else → fall through and scan.
|
|
86
|
+
if [ -n "$cmd" ] && ! is_git_commit "$cmd"; then
|
|
87
|
+
exit 0
|
|
31
88
|
fi
|
|
32
89
|
|
|
33
90
|
if ! command -v jscpd >/dev/null 2>&1; then
|