navori 0.8.6 → 0.8.7
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 +45 -1
- package/dist/assets/core/core-assets/agents/auditor.md +1 -1
- package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +16 -8
- package/dist/assets/core/core-assets/agents/explorer.md +1 -1
- package/dist/assets/core/core-assets/agents/implementer.md +1 -1
- package/dist/assets/core/core-assets/agents/researcher.md +1 -1
- package/dist/assets/core/core-assets/agents/reviewer.md +9 -0
- package/dist/assets/core/core-assets/hooks/_partials/audit-arm.sh +2 -2
- package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +45 -4
- package/dist/assets/core/core-assets/hooks/_partials/audit-repo.sh +17 -0
- package/dist/assets/core/core-assets/hooks/_partials/extract-cmd.sh +6 -6
- package/dist/assets/core/core-assets/hooks/_partials/scan-scope.sh +113 -0
- package/dist/assets/core/core-assets/hooks/audit-mode-close.sh +3 -1
- package/dist/assets/core/core-assets/hooks/audit-mode-trigger.sh +3 -1
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +1 -0
- package/dist/assets/core/core-assets/hooks/managed-drift-watch.sh +50 -2
- package/dist/assets/core/core-assets/hooks/pr-pilot-confirm.sh +1 -0
- package/dist/assets/core/core-assets/hooks/precompact-session-summary.sh +1 -0
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +5 -0
- package/dist/assets/core/core-assets/hooks/routing-watch.sh +115 -17
- package/dist/assets/core/core-assets/hooks/session-start-context.sh +1 -0
- package/dist/assets/core/core-assets/hooks/stop-verify-reminder.sh +1 -0
- package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +1 -0
- package/dist/assets/core/core-assets/hooks/worktree-reclaim.sh +1 -0
- package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
- package/dist/assets/core/core-assets/managed/formato-respuesta.md +1 -1
- package/dist/assets/core/core-assets/managed/orquestacion.md +13 -1
- package/dist/assets/core/core-assets/settings/settings-base.json +1 -3
- package/dist/assets/plugins/codegraph/managed/codegraph-protocol.md +1 -1
- package/dist/assets/plugins/engram/managed/engram-protocol.md +1 -1
- package/dist/assets/plugins/jscpd/plugin.json +8 -1
- package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +28 -30
- package/dist/assets/plugins/semgrep/plugin.json +8 -1
- package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +28 -42
- package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +1 -0
- package/dist/assets/plugins/tgrep/scripts/tgrep-session.sh +41 -0
- package/dist/index.js +384 -371
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ Y genera:
|
|
|
67
67
|
| `add <plugin>` | Activa un plugin y opcionalmente instala la tool externa |
|
|
68
68
|
| `remove <plugin>` | Desactiva un plugin y limpia sus bloques managed, sub-bloques y scripts |
|
|
69
69
|
| `configure <section>` | Ajusta una sección del config sin re-correr el wizard |
|
|
70
|
+
| `adopt <path>` | Toma un archivo de `.claude/` que escribiste a mano bajo gestión de navori: lo envuelve en un bloque managed sin reescribir su contenido (preview por default) |
|
|
70
71
|
| `update` | Re-detecta el repo, refresca config y corre sync en un paso |
|
|
71
72
|
| `render` | Genera los archivos nativos de cada engine configurado (preview por default; `--apply` escribe). `--all` renderea todos los repos del registro global; `--prune` limpia los que ya no existen |
|
|
72
73
|
| `registry <sub>` | Registro global de tus repos con navori, para `render --all` (`ls`, `scan <dir>`, `add`, `remove`, `prune`) |
|
|
@@ -75,6 +76,7 @@ Y genera:
|
|
|
75
76
|
| `scan` | Detecta workspaces nuevos en monorepos (`pnpm-workspace.yaml` / `package.json#workspaces`) |
|
|
76
77
|
| `doctor` | Audita el config + drift de cada managed block (CLAUDE.md **y AGENTS.md**), orden canónico, markers malformados, desincronización de monorepo y tools externas faltantes (`--strict` para CI) |
|
|
77
78
|
| `status` | Snapshot rápido: config, plugins activos, conteo de drift y próximos pasos |
|
|
79
|
+
| `audit` | Reporta cómo corrió el harness de verdad: atribución de tokens y huecos de adherencia en tus sesiones |
|
|
78
80
|
| `bench` | Corre `render` en dry-run N veces y reporta latencias (detecta regresiones locales) |
|
|
79
81
|
| `workspace <sub>` | Gestiona workspaces cross-repo (`init`, `ls`, `show`, `rm`) |
|
|
80
82
|
| `ticket <sub>` | Gestiona tickets-as-files en un workspace (`new`, `list`, `show`, `archive`, `delete`) |
|
|
@@ -125,6 +127,7 @@ La resolución es **local → bundled**: si tienes un preset local con el mismo
|
|
|
125
127
|
|---|---|---|
|
|
126
128
|
| `engram` | Memoria persistente entre sesiones | `engram` binary |
|
|
127
129
|
| `codegraph` | Grafo AST del repo vía MCP: símbolos, call paths y blast-radius en una llamada | `codegraph` |
|
|
130
|
+
| `tgrep` | Búsqueda de contenido indexada por trigramas (flags de ripgrep), con fallback automático si falta el binario | `tgrep` |
|
|
128
131
|
| `acli` | Leer tickets de Jira desde la terminal | `acli` |
|
|
129
132
|
| `gh` | GitHub Issues, PRs y workflow runs | `gh` |
|
|
130
133
|
| `jscpd` | Detección de duplicación en el diff | `jscpd` (opt-in) |
|
|
@@ -245,7 +248,7 @@ del baseline y de los prompts.
|
|
|
245
248
|
Qué escribe el `init --apply`, y nada más:
|
|
246
249
|
|
|
247
250
|
- `~/.navori/global.json` — el manifest: idioma, bloques del baseline y tus permisos globales.
|
|
248
|
-
- `~/.claude/skills/navori/` — el plugin `navori@skills-dir` con los 8 agentes, las
|
|
251
|
+
- `~/.claude/skills/navori/` — el plugin `navori@skills-dir` con los 8 agentes, las 11 skills y el
|
|
249
252
|
hook del baseline. Claude Code lo carga sin marketplace ni paso de instalación; las skills globales
|
|
250
253
|
se invocan `/navori:<nombre>` (tras un render, `/reload-plugins` o sesión nueva).
|
|
251
254
|
- `~/.claude/settings.json` — **solo** la clave `permissions`, y solo si declaraste permisos globales
|
|
@@ -301,6 +304,47 @@ navori configure pr-target develop # rama destino del PR (gh pr create --base
|
|
|
301
304
|
navori configure workspace bonum # asociar a un workspace
|
|
302
305
|
```
|
|
303
306
|
|
|
307
|
+
## Extender el harness en tu repo
|
|
308
|
+
|
|
309
|
+
navori instala un baseline; lo que lo vuelve valioso en **tu** repo es el conocimiento que sólo
|
|
310
|
+
tú tienes. Hay cuatro destinos, ordenados de más barato a más caro en archivos, revisión y tokens
|
|
311
|
+
por sesión. Empieza arriba de la tabla: el escalón más barato suele ser además el más efectivo.
|
|
312
|
+
|
|
313
|
+
| Lo que tienes | Dónde va |
|
|
314
|
+
|---|---|
|
|
315
|
+
| Una regla de tu repo (un patrón propio, la convención de tu data layer) | la **user-section** de la skill que ya cubre el tema |
|
|
316
|
+
| Conocimiento que ninguna skill instalada cubre | **skill project-local** |
|
|
317
|
+
| Conocimiento de un stack, reusable entre repos | **preset local** (`navori preset init <id>`) |
|
|
318
|
+
| Envoltura de un binario o servidor MCP | **plugin** (va a navori, no a tu repo) |
|
|
319
|
+
|
|
320
|
+
**La user-section es el default.** Cada skill que navori renderiza trae un sentinel
|
|
321
|
+
`<!-- navori:user-section -->`; todo lo que escribas después es tuyo y `render`/`sync` no lo tocan
|
|
322
|
+
nunca. Cero archivos nuevos, cero config, y la regla queda donde el agente ya iba a mirar.
|
|
323
|
+
|
|
324
|
+
**Una skill project-local** son dos pasos:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# 1. la forma DIRECTORIO es la única que el host descubre.
|
|
328
|
+
# Un `<id>.md` suelto en .claude/skills/ no se carga nunca.
|
|
329
|
+
mkdir -p .claude/skills/mi-skill && $EDITOR .claude/skills/mi-skill/SKILL.md
|
|
330
|
+
|
|
331
|
+
# 2. declara el id en navori.config.json:
|
|
332
|
+
# "project": { "localSkills": ["mi-skill"] }
|
|
333
|
+
navori doctor # valida que el archivo exista y que su description diga CUÁNDO usarla
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Su frontmatter necesita `name`, `type` (`behavior` \| `reference` \| `tool`) y una `description`
|
|
337
|
+
con **trigger de activación**. El host carga las skills on-demand leyendo esa línea, así que un
|
|
338
|
+
*"Usar cuando…"* es lo que la pone a trabajar sola en el momento justo. `navori doctor` te avisa
|
|
339
|
+
cuando a una le falta, que suele ser el arreglo de mayor retorno: el contenido ya está escrito.
|
|
340
|
+
|
|
341
|
+
navori **nunca escribe dentro** de una skill project-local: no lleva bloque managed ni
|
|
342
|
+
user-section, es tuya entera.
|
|
343
|
+
|
|
344
|
+
→ Guía completa (con las cuatro preguntas que hacen fuerte a una propuesta):
|
|
345
|
+
[`docs/EXTENDING.md`](https://github.com/UlisesCm/navori-harness/blob/main/docs/EXTENDING.md).
|
|
346
|
+
Contrato del `SKILL.md`: [`docs/recipes/skill-authoring.md`](https://github.com/UlisesCm/navori-harness/blob/main/docs/recipes/skill-authoring.md).
|
|
347
|
+
|
|
304
348
|
## Filosofía
|
|
305
349
|
|
|
306
350
|
- **Cero opinión sobre tu proceso**. El CLI detecta y propone; tú decides.
|
|
@@ -109,7 +109,7 @@ If the finding depends on a dependency's behavior, **verify its docs with `WebFe
|
|
|
109
109
|
|
|
110
110
|
2. **Prioritized plan** — `.claude/progress/plan_<scope>.md`: blockers (CRITICAL) → quick wins (low-effort HIGH/MEDIUM) → SDD features → cleanup (LOW). Each item with severity, files to touch, effort, and originating finding.
|
|
111
111
|
|
|
112
|
-
3. **SDD drafts (optional)** — for CRITICAL/HIGH findings that are SDD-scope
|
|
112
|
+
3. **SDD drafts (optional)** — only when SDD is enabled for this repo, for CRITICAL/HIGH findings that are SDD-scope write `{{sdd.specsDir}}/<feature>/{requirements,tasks}.md.draft`. The main agent refines them and drops the `.draft`.
|
|
113
113
|
|
|
114
114
|
## Hard rules
|
|
115
115
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: commit-pr-pilot
|
|
3
|
-
description: Drafts
|
|
3
|
+
description: Drafts commits in the configured style and opens the PR with the repo's title + body format, after a git/gh pre-flight. Does not edit project code. Use after the reviewer approves, when the cycle ends in a commit, a push or a PR.
|
|
4
4
|
tools: Read, Glob, Grep, Bash
|
|
5
5
|
model: {{models.commitPrPilot}}
|
|
6
6
|
effort: {{effort.commitPrPilot}}
|
|
@@ -8,7 +8,7 @@ effort: {{effort.commitPrPilot}}
|
|
|
8
8
|
|
|
9
9
|
# Commit & PR Pilot Agent
|
|
10
10
|
|
|
11
|
-
You own the **end of the cycle**: well-structured
|
|
11
|
+
You own the **end of the cycle**: well-structured commits in the configured style and PRs with a title + body that match the repo's format. You run pre-flight, validate, and fire `git`/`gh`. You don't edit project code.
|
|
12
12
|
|
|
13
13
|
## When to trigger
|
|
14
14
|
|
|
@@ -33,11 +33,20 @@ Run these checks before drafting anything. If something fails, you stop and repo
|
|
|
33
33
|
git status --porcelain # what's left to commit
|
|
34
34
|
git rev-parse --abbrev-ref HEAD # cannot be {{prTarget}}, the fork point, or any protected branch
|
|
35
35
|
git fetch origin {{prTarget}} --quiet
|
|
36
|
+
behind=$(git rev-list --count HEAD..origin/{{prTarget}})
|
|
37
|
+
if [ "$behind" -ne 0 ]; then
|
|
38
|
+
printf 'ABORT: branch is %s commit(s) behind origin/{{prTarget}}; integrate the target before committing or creating a PR.\n' "$behind" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
36
41
|
git log origin/{{prTarget}}..HEAD --oneline # must have ≥1 commit (or changes to commit)
|
|
37
42
|
git diff origin/{{prTarget}} --stat # REAL scope so far (two-dot: see below)
|
|
38
43
|
gh auth status # gh authenticated
|
|
39
44
|
```
|
|
40
45
|
|
|
46
|
+
A nonzero `behind` count is a hard stop: do not construct a shipping diff,
|
|
47
|
+
consume a receipt, commit, push, or create a PR. The reviewer would otherwise
|
|
48
|
+
have signed target-only files as phantom deletions from this stale worktree.
|
|
49
|
+
|
|
41
50
|
### The shipping diff — the one set every count in this pre-flight comes from
|
|
42
51
|
|
|
43
52
|
Coverage of the review and the receipt's fingerprints are two questions about the SAME set of files. Write it once, read it everywhere:
|
|
@@ -135,10 +144,9 @@ Never open the PR with the gate red.
|
|
|
135
144
|
|
|
136
145
|
1. Read `.claude/progress/impl_<feature>.md` to understand what changed and why.
|
|
137
146
|
2. Look at `git diff --stat` to confirm the scope.
|
|
138
|
-
3. Draft
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
- Description: imperative, ≤70 chars, no trailing period, language defined by `{{commits}}`.
|
|
147
|
+
3. Draft an atomic commit message in the configured style (`{{commits}}`).
|
|
148
|
+
- When the configured style is Conventional, use a lowercase type and scope derived from the touched area.
|
|
149
|
+
- Keep the description imperative, ≤70 chars and without a trailing period.
|
|
142
150
|
- Optional body with the WHY if the decision isn't obvious.
|
|
143
151
|
4. If you touch potentially sensitive files (`.env*`, credentials, odd lockfiles), **flag the user before staging**.
|
|
144
152
|
5. `git add <files>` (prefer explicit over `git add -A`).
|
|
@@ -167,7 +175,7 @@ Never open the PR with the gate red.
|
|
|
167
175
|
- `.claude/progress/impl_<feature>.md` if it exists — non-obvious decisions.
|
|
168
176
|
|
|
169
177
|
2. **Draft title and body**:
|
|
170
|
-
- **Title**:
|
|
178
|
+
- **Title**: follows the configured commit style (`{{commits}}`), ≤70 chars, imperative and without a trailing period.
|
|
171
179
|
- **Body**: the repo's exact template (below). No empty sections.
|
|
172
180
|
|
|
173
181
|
3. **Validate** before firing `gh`:
|
|
@@ -262,7 +270,7 @@ wc -c CLAUDE.md # after
|
|
|
262
270
|
- ❌ Never skip hooks (`--no-verify`) unless the user explicitly asks.
|
|
263
271
|
- ❌ Never ask for a merge / approve the PR yourself. Your job ends with the URL.
|
|
264
272
|
- ❌ Never `gh pr checks --watch`: it takes no timeout and would hang the turn before the URL reaches the user.
|
|
265
|
-
- ✅ Commit and PR message
|
|
273
|
+
- ✅ Commit and PR message follow the configured style (`{{commits}}`; `conventional-es` = Spanish MX, `conventional` = English). The `Closes #<N>` keyword is the exception: GitHub parses it and it stays in English (see the body template).
|
|
266
274
|
- ✅ If you introduce a new pattern or non-obvious decision that wasn't already in `impl_<feature>.md`, leave a note in the PR body ("Decisions" section).
|
|
267
275
|
|
|
268
276
|
## Anti-patterns
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: explorer
|
|
3
3
|
description: Maps a broad area or module — structure, key files, entry points, dependencies. Does not modify code. Use when you don't know where something lives and would otherwise open 4+ files to find out.
|
|
4
|
-
tools: Read, Glob, Grep, Bash, Write
|
|
4
|
+
tools: Read, Glob, Grep, Bash, Write, mcp__engram__mem_search, mcp__engram__mem_get_observation
|
|
5
5
|
model: {{models.explorer}}
|
|
6
6
|
effort: {{effort.explorer}}
|
|
7
7
|
---
|
|
@@ -100,7 +100,7 @@ Write `.claude/progress/impl_<feature>.md`:
|
|
|
100
100
|
- ...
|
|
101
101
|
|
|
102
102
|
## Suggested commit
|
|
103
|
-
|
|
103
|
+
`<configured commit style>` (atomic, language/style per `{{commits}}`)
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
## Communication with the leader
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: researcher
|
|
3
3
|
description: Answers ONE scoped question about the repo with cited evidence, written to a file. Does not modify code. Use when answering would take reading 4+ files, or to challenge a design decision with fresh context.
|
|
4
|
-
tools: Read, Glob, Grep, Bash, Write
|
|
4
|
+
tools: Read, Glob, Grep, Bash, Write, mcp__engram__mem_search, mcp__engram__mem_get_observation
|
|
5
5
|
model: {{models.researcher}}
|
|
6
6
|
effort: {{effort.researcher}}
|
|
7
7
|
---
|
|
@@ -25,6 +25,11 @@ You are a strict reviewer. Your only function is to **approve or reject**. You d
|
|
|
25
25
|
```bash
|
|
26
26
|
git status --short
|
|
27
27
|
git fetch origin {{prTarget}} --quiet
|
|
28
|
+
behind=$(git rev-list --count HEAD..origin/{{prTarget}})
|
|
29
|
+
if [ "$behind" -ne 0 ]; then
|
|
30
|
+
printf 'ABORT: branch is %s commit(s) behind origin/{{prTarget}}; integrate the target before reviewing.\n' "$behind" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
28
33
|
git diff --stat
|
|
29
34
|
# two-dot: the FULL working tree vs the target (committed AND uncommitted),
|
|
30
35
|
# the exact set the receipt fingerprints below. Three-dot (`...HEAD`) would show
|
|
@@ -34,6 +39,10 @@ You are a strict reviewer. Your only function is to **approve or reject**. You d
|
|
|
34
39
|
git ls-files --others --exclude-standard # untracked files (new, not yet staged)
|
|
35
40
|
```
|
|
36
41
|
|
|
42
|
+
A nonzero `behind` count is a hard stop: do not review, approve, or write a
|
|
43
|
+
receipt. A target-only file would otherwise look like a deletion in this
|
|
44
|
+
worktree and the receipt would sign that phantom deletion.
|
|
45
|
+
|
|
37
46
|
3. **Re-review** (if there's already a `.claude/progress/review_<feature>.md` from a previous cycle): focus the *reading* on (a) that the issues listed there are resolved and (b) the files the `implementer` reports having touched in this cycle (`impl_<feature>.md`). Don't re-review from scratch the already-approved code that didn't change; the full quality gate is still run anyway — a change can break something outside the delta. If the previous verdict was already `APPROVED` and the diff only moved because of an edit made after it, that's the **delta re-sign** mode below, not this one.
|
|
38
47
|
4. Apply `.claude/skills/verify-before-done/SKILL.md` to every `[x]` that depends on evidence. The quality gate is run **this turn, in Pass 2** (not before: a `SPEC_MISS` in Pass 1 doesn't need it — don't spend the gate on a diff you're going to reject on spec). Don't assume from the implementer's cached report.
|
|
39
48
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# charset (#503) — this function trusts it into a command line, so an unvalidated
|
|
18
18
|
# id must never reach here. $2 is the payload's cwd (#454: never
|
|
19
19
|
# CLAUDE_PROJECT_DIR — they differ in worktrees, and --arm wrote the flag under
|
|
20
|
-
# the name
|
|
20
|
+
# the repo name resolved from the cwd). $3 is the audits root.
|
|
21
21
|
#
|
|
22
22
|
# Fail-open and silent: returns 0 ONLY when audit-mode was actually started, so
|
|
23
23
|
# the caller can announce it; every other path returns 1 and changes nothing.
|
|
@@ -27,7 +27,7 @@ navori_audit_consume_armed() {
|
|
|
27
27
|
narm_cwd=$2
|
|
28
28
|
narm_root=$3
|
|
29
29
|
[ -n "$narm_sid" ] && [ -n "$narm_cwd" ] && [ -n "$narm_root" ] || return 1
|
|
30
|
-
narm_repo=$(
|
|
30
|
+
narm_repo=$(navori_audit_repo_from_cwd "$narm_cwd") || return 1
|
|
31
31
|
[ -n "$narm_repo" ] || return 1
|
|
32
32
|
narm_file=$narm_root/$narm_repo/.armed
|
|
33
33
|
[ -f "$narm_file" ] || return 1
|
|
@@ -122,7 +122,7 @@ navori_audit_log() {
|
|
|
122
122
|
# and the chain yields `""` — the field then disappears from the record and
|
|
123
123
|
# `ownerOf` falls back to the time window, which is the guess this field
|
|
124
124
|
# exists to avoid. Caught by a test, not by review.
|
|
125
|
-
navori_audit_fields=$(printf '%s' "${payload:-}" | jq -r '[.session_id // "", .cwd // "", ([.agent_id, .subagent_id] | map(select(type == "string" and . != "")) | first) // "orchestrator", "."] | .[]' 2>/dev/null) || return 0
|
|
125
|
+
navori_audit_fields=$(printf '%s' "${payload:-}" | jq -r '[.session_id // "", .cwd // "", ([.agent_id, .subagent_id] | map(select(type == "string" and . != "")) | first) // "orchestrator", .tool_use_id // "", "."] | .[]' 2>/dev/null) || return 0
|
|
126
126
|
navori_audit_session=${navori_audit_fields%%
|
|
127
127
|
*}
|
|
128
128
|
navori_audit_rest=${navori_audit_fields#*
|
|
@@ -132,6 +132,10 @@ navori_audit_log() {
|
|
|
132
132
|
navori_audit_rest=${navori_audit_rest#*
|
|
133
133
|
}
|
|
134
134
|
navori_audit_agent=${navori_audit_rest%%
|
|
135
|
+
*}
|
|
136
|
+
navori_audit_rest=${navori_audit_rest#*
|
|
137
|
+
}
|
|
138
|
+
navori_audit_tool_use_id=${navori_audit_rest%%
|
|
135
139
|
*}
|
|
136
140
|
[ -n "$navori_audit_session" ] || return 0
|
|
137
141
|
# Same character class the CLI enforces (#503): the id composes a path, so
|
|
@@ -141,7 +145,7 @@ navori_audit_log() {
|
|
|
141
145
|
esac
|
|
142
146
|
|
|
143
147
|
[ -n "$navori_audit_cwd" ] || navori_audit_cwd=$PWD
|
|
144
|
-
navori_audit_repo=$(
|
|
148
|
+
navori_audit_repo=$(navori_audit_repo_from_cwd "$navori_audit_cwd") || return 0
|
|
145
149
|
[ -n "$navori_audit_repo" ] || return 0
|
|
146
150
|
|
|
147
151
|
navori_audit_file=$navori_audit_root/$navori_audit_repo/session-$navori_audit_session.log
|
|
@@ -149,7 +153,42 @@ navori_audit_log() {
|
|
|
149
153
|
# The session may not be the marked one even in a repo that has been audited
|
|
150
154
|
# before. Also the writability check — a log that cannot be appended to is not
|
|
151
155
|
# an error, it is simply not recording.
|
|
152
|
-
[ -f "$navori_audit_file" ]
|
|
156
|
+
if [ ! -f "$navori_audit_file" ]; then
|
|
157
|
+
# SPOOL for the phase that CANNOT have a log yet (#778).
|
|
158
|
+
#
|
|
159
|
+
# `navori audit --start` is what creates the session log, and it runs from
|
|
160
|
+
# the UserPromptSubmit hook — i.e. after the first prompt. Every SessionStart
|
|
161
|
+
# hook therefore fires BEFORE the file exists, and the check above threw its
|
|
162
|
+
# record away every single time: measured, `session-start-context` recorded 1
|
|
163
|
+
# of ~20 startups in this repo, and the only survivor was a resume onto an
|
|
164
|
+
# already-open log. "Did the session load the harness?" had no witness at all,
|
|
165
|
+
# which is exactly the question the recorder exists to answer.
|
|
166
|
+
#
|
|
167
|
+
# So those records go to a side file that `--start` absorbs. Two deliberate
|
|
168
|
+
# limits keep this from becoming a leak:
|
|
169
|
+
#
|
|
170
|
+
# 1. SessionStart ONLY. Every other phase runs after a prompt, so a missing
|
|
171
|
+
# log there means the session is genuinely not marked — and spooling
|
|
172
|
+
# those would write four lines per Bash call, for every session of every
|
|
173
|
+
# repo, forever. That is thousands of writes to buy nothing.
|
|
174
|
+
# 2. Only where the repo's audit directory ALREADY exists, which means the
|
|
175
|
+
# repo has been audited (or armed) at least once. `mkdir` is never run
|
|
176
|
+
# from here: a repo that has never used audit-mode must stay at zero
|
|
177
|
+
# files and zero forks, the same contract as the root gate above. The
|
|
178
|
+
# cost is that the FIRST audited session of a repo still loses its
|
|
179
|
+
# SessionStart records; every one after it has them.
|
|
180
|
+
#
|
|
181
|
+
# FAIL-OPEN, and here more than anywhere: this runs while a session is
|
|
182
|
+
# opening. Every failure path below returns 0 and writes nothing to stdout —
|
|
183
|
+
# a spool that could abort a hook would make observation the reason a session
|
|
184
|
+
# does not start, which is the one bug this partial may never have.
|
|
185
|
+
[ "${navori_audit_phase:-}" = "SessionStart" ] || return 0
|
|
186
|
+
[ -d "$navori_audit_root/$navori_audit_repo" ] || return 0
|
|
187
|
+
navori_audit_file=$navori_audit_root/$navori_audit_repo/pending-$navori_audit_session.jsonl
|
|
188
|
+
if [ ! -e "$navori_audit_file" ]; then
|
|
189
|
+
: >> "$navori_audit_file" 2>/dev/null || return 0
|
|
190
|
+
fi
|
|
191
|
+
fi
|
|
153
192
|
[ -w "$navori_audit_file" ] || return 0
|
|
154
193
|
|
|
155
194
|
# Volume valve, OFF by default.
|
|
@@ -218,12 +257,14 @@ navori_audit_log() {
|
|
|
218
257
|
--arg tool "${navori_audit_tool:-}" \
|
|
219
258
|
--arg src "${navori_audit_source:-core}" \
|
|
220
259
|
--arg agent "${navori_audit_agent:-}" \
|
|
260
|
+
--arg toolUseId "${navori_audit_tool_use_id:-}" \
|
|
221
261
|
--argjson ms "$navori_audit_ms" \
|
|
222
262
|
--argjson tsMs "$navori_audit_end" \
|
|
223
263
|
'{tsMs:$tsMs,event:"hook",name:$name,phase:$phase,verdict:$verdict,ms:$ms,source:$src}
|
|
224
264
|
+ (if $tool == "" then {} else {tool:$tool} end)
|
|
225
265
|
+ (if $reason == "" then {} else {reason:$reason} end)
|
|
226
|
-
+ (if $agent == "" then {} else {agentId:$agent} end)
|
|
266
|
+
+ (if $agent == "" then {} else {agentId:$agent} end)
|
|
267
|
+
+ (if $toolUseId == "" then {} else {toolUseId:$toolUseId} end)' 2>/dev/null)" \
|
|
227
268
|
>> "$navori_audit_file" 2>/dev/null
|
|
228
269
|
|
|
229
270
|
return 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Shared audit repository resolver (#764) — inlined into every audit hook at
|
|
2
|
+
# render time. A nested agent worktree lives below the repository's
|
|
3
|
+
# `.claude/worktrees/` directory, but its basename is an ephemeral agent id.
|
|
4
|
+
#
|
|
5
|
+
# navori_audit_repo_from_cwd <cwd> — prints the stable parent repo name.
|
|
6
|
+
# This only uses shell builtins before the existing `basename` call: audit hooks
|
|
7
|
+
# run often, so discovering the Git common directory would add an avoidable fork
|
|
8
|
+
# per invocation.
|
|
9
|
+
navori_audit_repo_from_cwd() {
|
|
10
|
+
navori_audit_repo_cwd=$1
|
|
11
|
+
case "$navori_audit_repo_cwd" in
|
|
12
|
+
*/.claude/worktrees | */.claude/worktrees/*)
|
|
13
|
+
navori_audit_repo_cwd=${navori_audit_repo_cwd%%/.claude/worktrees*}
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
16
|
+
basename "$navori_audit_repo_cwd" 2>/dev/null
|
|
17
|
+
}
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
# Generic on purpose: `.cwd` feeds the worktree resolver of #454 through the
|
|
14
14
|
# SAME hardened cascade instead of a second copy of it.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
16
|
+
# The sed fallback reads a JSON string through its first unescaped quote. JSON
|
|
17
|
+
# object member order is not a host contract: `command` can precede `cwd`, so a
|
|
18
|
+
# greedy capture to the last quote would swallow the rest of the payload when
|
|
19
|
+
# neither jq nor node is available.
|
|
20
20
|
payload=$(cat)
|
|
21
21
|
payload_field() {
|
|
22
22
|
if command -v jq >/dev/null 2>&1; then
|
|
@@ -25,10 +25,10 @@ payload_field() {
|
|
|
25
25
|
if command -v node >/dev/null 2>&1; then
|
|
26
26
|
printf '%s' "$payload" | node -e 'let s="";const p=process.argv[1].split(".");process.stdin.on("data",c=>s+=c).on("end",()=>{try{let v=JSON.parse(s);for(const k of p)v=v?.[k];process.stdout.write(String(v??""))}catch{}})' "$1" 2>/dev/null && return 0
|
|
27
27
|
fi
|
|
28
|
-
printf '%s' "$payload" | sed -
|
|
28
|
+
printf '%s' "$payload" | sed -nE "s/.*\"${1##*.}\"[[:space:]]*:[[:space:]]*\"(([^\"\\]|\\.)*)\".*/\\1/p"
|
|
29
29
|
}
|
|
30
30
|
extract_cmd() {
|
|
31
|
-
payload_field tool_input.command
|
|
31
|
+
payload_field tool_input.command
|
|
32
32
|
}
|
|
33
33
|
# NOT called here on purpose. `payload_field` may spawn a process, and
|
|
34
34
|
# `routing-watch.sh` — which includes this partial and runs after EVERY tool call
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Shared scan scope — inlined into the diff-scanning gate hooks at render time
|
|
2
|
+
# (see the include directive in the source scripts + lib/hook-includes.ts).
|
|
3
|
+
# DO NOT copy this body back into a hook by hand: hand-synced copies are how the
|
|
4
|
+
# two scanners drifted apart in the first place (#225/#261, and #777 below).
|
|
5
|
+
#
|
|
6
|
+
# Single source of truth for the two questions every diff scanner answers before
|
|
7
|
+
# it runs: WHAT is the baseline, and WHICH files are in scope. `check-semgrep`
|
|
8
|
+
# and `check-jscpd` used to answer both with copies that had already diverged —
|
|
9
|
+
# jscpd never resolved its base to a sha, so its cache and its scan could name
|
|
10
|
+
# different commits.
|
|
11
|
+
#
|
|
12
|
+
# Requires, from the caller: `$base` (the configured base branch, shell-quoted
|
|
13
|
+
# at render time via the `shq:` marker) and a cwd already inside the working
|
|
14
|
+
# tree being scanned. `$navori_scan_label` names the caller in every message.
|
|
15
|
+
|
|
16
|
+
# --- Baseline (#777) ---------------------------------------------------------
|
|
17
|
+
# `origin/<base>` FIRST, the local ref only as a fallback.
|
|
18
|
+
#
|
|
19
|
+
# The local ref carries NO freshness guarantee. An agent worktree is cut from
|
|
20
|
+
# whatever `main` pointed at that moment and never moves again, and a main
|
|
21
|
+
# checkout nobody pulls is the same shape. A baseline taken from it drifts
|
|
22
|
+
# behind every change that lands meanwhile, and the scan then compares against
|
|
23
|
+
# a tree that no longer exists anywhere: the diff includes files OTHER changes
|
|
24
|
+
# merged, so the gate can BLOCK on findings that are not this diff's — or
|
|
25
|
+
# accept, as "already at the baseline", a finding this diff introduces. Both
|
|
26
|
+
# happened.
|
|
27
|
+
#
|
|
28
|
+
# NO network here, on purpose. This runs as a `PreToolUse` hook on every
|
|
29
|
+
# `git commit`, so a `fetch` would put a remote round-trip — and its timeouts,
|
|
30
|
+
# its auth prompts, its outages — in front of every commit in the repo.
|
|
31
|
+
# `origin/<base>` is kept fresh by the pre-flight of the reviewer and the
|
|
32
|
+
# commit-pr-pilot, which both fetch before they work; in that cycle the ref is
|
|
33
|
+
# already current when the hook reads it. Outside it (an offline clone, a repo
|
|
34
|
+
# with no remote) the fallback keeps the previous behaviour exactly.
|
|
35
|
+
#
|
|
36
|
+
# Sets `$base_ref` (the ref actually used, for messages) and `$base_sha` (its
|
|
37
|
+
# commit — resolved to a sha because it is BOTH the scan's baseline and part of
|
|
38
|
+
# the content cache's fingerprint, so it has to name one immutable commit).
|
|
39
|
+
# Returns 1 when neither ref resolves; the caller skips.
|
|
40
|
+
navori_resolve_base() {
|
|
41
|
+
local local_sha="" local_short="" origin_short=""
|
|
42
|
+
base_ref="$base"
|
|
43
|
+
base_sha=$(git rev-parse --verify --quiet "origin/$base^{commit}" 2>/dev/null || true)
|
|
44
|
+
if [ -n "$base_sha" ]; then
|
|
45
|
+
base_ref="origin/$base"
|
|
46
|
+
local_sha=$(git rev-parse --verify --quiet "$base^{commit}" 2>/dev/null || true)
|
|
47
|
+
# The lag itself is normal; the lag being SILENT is what cost a day. One
|
|
48
|
+
# line, only when the two refs actually disagree, so "why was that file
|
|
49
|
+
# scanned" has an answer in the transcript instead of a reconstruction.
|
|
50
|
+
if [ -n "$local_sha" ] && [ "$local_sha" != "$base_sha" ]; then
|
|
51
|
+
local_short=$(git rev-parse --short "$local_sha" 2>/dev/null || printf '%s' "$local_sha")
|
|
52
|
+
origin_short=$(git rev-parse --short "$base_sha" 2>/dev/null || printf '%s' "$base_sha")
|
|
53
|
+
echo "ℹ $navori_scan_label: local '$base' ($local_short) ≠ origin/$base ($origin_short) — baseline is origin/$base" >&2
|
|
54
|
+
fi
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
base_sha=$(git rev-parse --verify --quiet "$base^{commit}" 2>/dev/null || true)
|
|
58
|
+
[ -n "$base_sha" ] || return 1
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# --- Files in scope (#777) ---------------------------------------------------
|
|
63
|
+
# `git diff` for tracked changes PLUS `git ls-files --others` for untracked
|
|
64
|
+
# ones.
|
|
65
|
+
#
|
|
66
|
+
# The untracked half is not an edge case. These hooks are `PreToolUse`: they run
|
|
67
|
+
# BEFORE the command they gate. With `git add new.ts && git commit -m x` in ONE
|
|
68
|
+
# Bash call — the batching the harness itself recommends in auto mode — the file
|
|
69
|
+
# is still untracked when the scan runs, so a list built from `git diff` alone
|
|
70
|
+
# reported `0 files to scan` and the gate went green over a file nobody read.
|
|
71
|
+
# Agent work is mostly new files, which made that the common case rather than
|
|
72
|
+
# the rare one. `--exclude-standard` keeps ignored build output out: generated
|
|
73
|
+
# artifacts are not new code.
|
|
74
|
+
#
|
|
75
|
+
# The two lists are disjoint by construction (`--others` is precisely what the
|
|
76
|
+
# index does not track), so there is nothing to de-duplicate.
|
|
77
|
+
#
|
|
78
|
+
# Both listings carry a sentinel with their exit status: they run inside a
|
|
79
|
+
# process substitution, whose status the shell never reports, so a FAILED
|
|
80
|
+
# listing (exit 128: unborn HEAD, a corrupt index, a base that vanished
|
|
81
|
+
# mid-run) would otherwise be indistinguishable from "nothing changed" — the
|
|
82
|
+
# gate printing `0 files to scan` and exiting 0 over a tree it never read
|
|
83
|
+
# (#511). The sentinel can never collide with a real record: the pathspec
|
|
84
|
+
# restricts both lists to `*.ts`/`*.tsx`.
|
|
85
|
+
#
|
|
86
|
+
# Sets `$files` (array) and, on failure, `$scan_files_status`. Returns 1 when
|
|
87
|
+
# either listing failed, so the caller can report it as "nothing was scanned"
|
|
88
|
+
# rather than as a verdict.
|
|
89
|
+
navori_collect_scan_files() {
|
|
90
|
+
local sentinel='@navori-scan-status:' f=""
|
|
91
|
+
scan_files_status=""
|
|
92
|
+
files=()
|
|
93
|
+
while IFS= read -r -d '' f; do
|
|
94
|
+
case "$f" in
|
|
95
|
+
"${sentinel}"*)
|
|
96
|
+
scan_files_status="${f#"${sentinel}"}"
|
|
97
|
+
continue
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
files+=("$f")
|
|
101
|
+
done < <(
|
|
102
|
+
# `|| rc=$?` and not a bare `$?`: the subshell inherits `set -e`, which
|
|
103
|
+
# would kill it on a failing git call before the sentinel is ever written.
|
|
104
|
+
rc=0
|
|
105
|
+
git diff --name-only -z --diff-filter=ACMRT "$base_sha" -- '*.ts' '*.tsx' || rc=$?
|
|
106
|
+
if [ "$rc" -eq 0 ]; then
|
|
107
|
+
git ls-files --others --exclude-standard -z -- '*.ts' '*.tsx' || rc=$?
|
|
108
|
+
fi
|
|
109
|
+
printf '%s%s\0' "$sentinel" "$rc"
|
|
110
|
+
)
|
|
111
|
+
[ "$scan_files_status" = "0" ] || return 1
|
|
112
|
+
return 0
|
|
113
|
+
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
set +e
|
|
12
12
|
|
|
13
|
+
# navori:include audit-repo
|
|
14
|
+
|
|
13
15
|
payload=$(cat 2>/dev/null) || exit 0
|
|
14
16
|
[ -n "$payload" ] || exit 0
|
|
15
17
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
@@ -20,7 +22,7 @@ reason=$(printf '%s' "$payload" | jq -r '.reason // .matcher // "other"' 2>/dev/
|
|
|
20
22
|
[ -n "$session_id" ] || exit 0
|
|
21
23
|
[ -n "$cwd" ] || cwd=$PWD
|
|
22
24
|
|
|
23
|
-
repo=$(
|
|
25
|
+
repo=$(navori_audit_repo_from_cwd "$cwd") || exit 0
|
|
24
26
|
[ -n "$repo" ] || exit 0
|
|
25
27
|
|
|
26
28
|
if [ -n "$NAVORI_AUDITS_ROOT" ]; then
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
set +e
|
|
24
24
|
|
|
25
|
+
# navori:include audit-repo
|
|
26
|
+
|
|
25
27
|
payload=$(cat 2>/dev/null) || exit 0
|
|
26
28
|
[ -n "$payload" ] || exit 0
|
|
27
29
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
@@ -50,7 +52,7 @@ case "$session_id" in
|
|
|
50
52
|
esac
|
|
51
53
|
[ -n "$cwd" ] || cwd=$PWD
|
|
52
54
|
|
|
53
|
-
repo=$(
|
|
55
|
+
repo=$(navori_audit_repo_from_cwd "$cwd") || exit 0
|
|
54
56
|
[ -n "$repo" ] || exit 0
|
|
55
57
|
|
|
56
58
|
if [ -n "$NAVORI_AUDITS_ROOT" ]; then
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# PostToolUse
|
|
3
|
+
# PostToolUse watcher for managed-block drift (#530), on every tool that can
|
|
4
|
+
# write a file: `Bash|Edit|Write|NotebookEdit`.
|
|
5
|
+
#
|
|
6
|
+
# WHY THE MATCHER IS THE WHOLE DECISION, and why it took #775 to widen it. This
|
|
7
|
+
# script audits STATE, never the command, so it has no `case "$tool_name"` to
|
|
8
|
+
# contradict its registration — the matcher alone decides when it runs, which
|
|
9
|
+
# makes a narrow one indistinguishable from a hook that works. It was `Bash`
|
|
10
|
+
# only, and the host's own PostToolUse doc spells out the consequence from the
|
|
11
|
+
# other direction: a hook matching `Edit|Write` does not run when a `Bash`
|
|
12
|
+
# command rewrites the same file. The inverse is the hole this had — in
|
|
13
|
+
# `default`/`acceptEdits`, where the host does NOT push everything through the
|
|
14
|
+
# shell, an `Edit` over `CLAUDE.md` broke a managed block's hash and this stayed
|
|
15
|
+
# quiet until the next Bash call, if one ever came. A session of pure native
|
|
16
|
+
# editing put the #523 freeze back into silence, which is the one thing this
|
|
17
|
+
# hook exists to prevent.
|
|
18
|
+
#
|
|
19
|
+
# WHY NOT `FileChanged`, which the same doc section recommends for exactly this
|
|
20
|
+
# ("to run a hook when a specific file changes on disk, whatever wrote it").
|
|
21
|
+
# Because it would need the watchlist IN the settings, and the managed file set
|
|
22
|
+
# is derived per repo at render time (engine outputs plus every rendered asset,
|
|
23
|
+
# ~60 files here) — so settings.json would carry a second copy of a list this
|
|
24
|
+
# script already derives, and the two would drift. A watchlist that drifts from
|
|
25
|
+
# the managed set is a detector with holes in it, i.e. this hook's own defect
|
|
26
|
+
# class. It is also outside navori's neutral hook vocabulary (`lib/plugins.ts`
|
|
27
|
+
# admits four events, and every engine adapter maps those), so adopting it is a
|
|
28
|
+
# contract change rather than a matcher change.
|
|
29
|
+
#
|
|
30
|
+
# What the widening does NOT buy, stated plainly: a write from outside the
|
|
31
|
+
# session — a formatter in a watch process, another terminal — still surfaces
|
|
32
|
+
# only on the next tool call navori sees. `FileChanged` would catch that one.
|
|
33
|
+
# The cost of what it does buy is one shasum pass (~25ms, quoted below) per
|
|
34
|
+
# native edit.
|
|
4
35
|
#
|
|
5
36
|
# THIS HOOK NEVER READS THE COMMAND. That is the whole design. Its sibling
|
|
6
37
|
# `guard-destructive.sh` decides by the SHAPE of what you typed, so it only ever
|
|
@@ -46,7 +77,23 @@ payload=$(cat 2>/dev/null) || payload=""
|
|
|
46
77
|
|
|
47
78
|
navori_audit_name="managed-drift-watch"
|
|
48
79
|
navori_audit_phase="PostToolUse"
|
|
49
|
-
|
|
80
|
+
# The recorded tool used to be the literal "Bash", which was true while the
|
|
81
|
+
# matcher was. Now that four tools reach here it has to be read — and read
|
|
82
|
+
# without a fork, because this runs after every tool call: `#` and `%%` are
|
|
83
|
+
# builtins, and a tool name carries no quote to escape. Anything the parse does
|
|
84
|
+
# not recognise leaves the field EMPTY (the partial then omits it) rather than
|
|
85
|
+
# guessing, since a wrong label in the audit log is how a lane gets measured as
|
|
86
|
+
# working when it is not — this issue, one level up.
|
|
87
|
+
navori_audit_tool=""
|
|
88
|
+
case "$payload" in
|
|
89
|
+
*'"tool_name":"'*)
|
|
90
|
+
navori_audit_tool=${payload#*'"tool_name":"'}
|
|
91
|
+
navori_audit_tool=${navori_audit_tool%%'"'*}
|
|
92
|
+
case "$navori_audit_tool" in
|
|
93
|
+
"" | *[!A-Za-z_]*) navori_audit_tool="" ;;
|
|
94
|
+
esac
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
50
97
|
# Fallback no-ops, overwritten by the real definitions the include brings in.
|
|
51
98
|
# They exist because this hook is FAIL-OPEN: if the file ever runs WITHOUT its
|
|
52
99
|
# includes expanded — a raw copy of the asset, a render that half-finished — an
|
|
@@ -55,6 +102,7 @@ navori_audit_tool="Bash"
|
|
|
55
102
|
# never have.
|
|
56
103
|
navori_audit_begin() { :; }
|
|
57
104
|
navori_audit_log() { :; }
|
|
105
|
+
# navori:include audit-repo
|
|
58
106
|
# navori:include audit-log
|
|
59
107
|
navori_audit_begin
|
|
60
108
|
|
|
@@ -32,6 +32,7 @@ navori_audit_tool="Bash"
|
|
|
32
32
|
# never have.
|
|
33
33
|
navori_audit_begin() { :; }
|
|
34
34
|
navori_audit_log() { :; }
|
|
35
|
+
# navori:include audit-repo
|
|
35
36
|
# navori:include audit-log
|
|
36
37
|
navori_audit_begin
|
|
37
38
|
|
|
@@ -83,6 +84,10 @@ is_pm() {
|
|
|
83
84
|
|
|
84
85
|
run_gate() {
|
|
85
86
|
navori_audit_ran_gate=1
|
|
87
|
+
# The host can kill this process at its hook timeout, before our EXIT trap
|
|
88
|
+
# records allow/block. Persist a start marker first; audit correlates it with
|
|
89
|
+
# the terminal record by tool_use_id and reports an unfinished gate.
|
|
90
|
+
navori_audit_log "gate-started" "inicio del quality gate" || true
|
|
86
91
|
echo "[navori] running quality-gate fast: $1" >&2
|
|
87
92
|
eval "$1" || {
|
|
88
93
|
echo "[navori] quality-gate fast failed. Commit aborted." >&2
|