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.
Files changed (38) hide show
  1. package/README.md +45 -1
  2. package/dist/assets/core/core-assets/agents/auditor.md +1 -1
  3. package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +16 -8
  4. package/dist/assets/core/core-assets/agents/explorer.md +1 -1
  5. package/dist/assets/core/core-assets/agents/implementer.md +1 -1
  6. package/dist/assets/core/core-assets/agents/researcher.md +1 -1
  7. package/dist/assets/core/core-assets/agents/reviewer.md +9 -0
  8. package/dist/assets/core/core-assets/hooks/_partials/audit-arm.sh +2 -2
  9. package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +45 -4
  10. package/dist/assets/core/core-assets/hooks/_partials/audit-repo.sh +17 -0
  11. package/dist/assets/core/core-assets/hooks/_partials/extract-cmd.sh +6 -6
  12. package/dist/assets/core/core-assets/hooks/_partials/scan-scope.sh +113 -0
  13. package/dist/assets/core/core-assets/hooks/audit-mode-close.sh +3 -1
  14. package/dist/assets/core/core-assets/hooks/audit-mode-trigger.sh +3 -1
  15. package/dist/assets/core/core-assets/hooks/guard-destructive.sh +1 -0
  16. package/dist/assets/core/core-assets/hooks/managed-drift-watch.sh +50 -2
  17. package/dist/assets/core/core-assets/hooks/pr-pilot-confirm.sh +1 -0
  18. package/dist/assets/core/core-assets/hooks/precompact-session-summary.sh +1 -0
  19. package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +5 -0
  20. package/dist/assets/core/core-assets/hooks/routing-watch.sh +115 -17
  21. package/dist/assets/core/core-assets/hooks/session-start-context.sh +1 -0
  22. package/dist/assets/core/core-assets/hooks/stop-verify-reminder.sh +1 -0
  23. package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +1 -0
  24. package/dist/assets/core/core-assets/hooks/worktree-reclaim.sh +1 -0
  25. package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
  26. package/dist/assets/core/core-assets/managed/formato-respuesta.md +1 -1
  27. package/dist/assets/core/core-assets/managed/orquestacion.md +13 -1
  28. package/dist/assets/core/core-assets/settings/settings-base.json +1 -3
  29. package/dist/assets/plugins/codegraph/managed/codegraph-protocol.md +1 -1
  30. package/dist/assets/plugins/engram/managed/engram-protocol.md +1 -1
  31. package/dist/assets/plugins/jscpd/plugin.json +8 -1
  32. package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +28 -30
  33. package/dist/assets/plugins/semgrep/plugin.json +8 -1
  34. package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +28 -42
  35. package/dist/assets/plugins/tgrep/scripts/guard-search-routing.sh +1 -0
  36. package/dist/assets/plugins/tgrep/scripts/tgrep-session.sh +41 -0
  37. package/dist/index.js +384 -371
  38. 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 12 skills y el
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 (see the **Spec Driven Development** block in `CLAUDE.md`), write `{{sdd.specsDir}}/<feature>/{requirements,tasks}.md.draft`. The `leader` refines them and drops the `.draft`.
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 Conventional commits 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.
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 Conventional commits 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.
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 a Conventional commit message:
139
- - Type: `feat | fix | docs | refactor | perf | test | chore | style | build | ci | revert`.
140
- - Scope: lowercase, derived from the touched area (module/domain).
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**: Conventional Commits `type(scope): description`. ≤70 chars. Imperative. No trailing period.
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 in the language defined by `{{commits}}` (`conventional-es` = Spanish MX, `conventional` = English) — except the `Closes #<N>` keyword, which GitHub parses and which stays in English in any language (see the body template).
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
- `feat(<scope>): ...` (Conventional, atomic, language per `{{commits}}`)
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 basename(cwd) resolves to). $3 is the audits root.
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=$(basename "$narm_cwd" 2>/dev/null) || return 1
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=$(basename "$navori_audit_cwd" 2>/dev/null) || return 0
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" ] || return 0
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)' 2>/dev/null)" \
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
- # $2 overrides the sed fallback's capture. `.*` (greedy, to the last quote on the
17
- # line) is right for `command`, whose value can itself contain escaped quotes and
18
- # which Claude Code sends LAST. Every other field takes the default `[^"]*` run,
19
- # so a value with more JSON after it is not swallowed whole.
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 -n "s/.*\"${1##*.}\"[[:space:]]*:[[:space:]]*\"\(${2:-[^\"]*}\)\".*/\1/p"
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=$(basename "$cwd" 2>/dev/null) || exit 0
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=$(basename "$cwd" 2>/dev/null) || exit 0
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
@@ -43,6 +43,7 @@ navori_audit_tool="Bash"
43
43
  # never have.
44
44
  navori_audit_begin() { :; }
45
45
  navori_audit_log() { :; }
46
+ # navori:include audit-repo
46
47
  # navori:include audit-log
47
48
  navori_audit_begin
48
49
 
@@ -1,6 +1,37 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # PostToolUse(Bash) watcher for managed-block drift (#530).
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
- navori_audit_tool="Bash"
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
 
@@ -63,6 +63,7 @@ navori_audit_tool="Bash"
63
63
  # Same contract as the sibling gates: a recorder may never kill what it observes.
64
64
  navori_audit_begin() { :; }
65
65
  navori_audit_log() { :; }
66
+ # navori:include audit-repo
66
67
  # navori:include audit-log
67
68
  navori_audit_begin
68
69
 
@@ -36,6 +36,7 @@ navori_audit_phase="PreCompact"
36
36
  # never have.
37
37
  navori_audit_begin() { :; }
38
38
  navori_audit_log() { :; }
39
+ # navori:include audit-repo
39
40
  # navori:include audit-log
40
41
  navori_audit_begin
41
42
 
@@ -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