navori 0.6.4 → 0.7.0

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 (31) hide show
  1. package/README.md +46 -0
  2. package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +24 -2
  3. package/dist/assets/core/core-assets/agents/leader.md +2 -2
  4. package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +179 -0
  5. package/dist/assets/core/core-assets/hooks/audit-mode-trigger.sh +28 -80
  6. package/dist/assets/core/core-assets/hooks/guard-destructive.sh +42 -1
  7. package/dist/assets/core/core-assets/hooks/managed-drift-watch.sh +41 -0
  8. package/dist/assets/core/core-assets/hooks/precompact-session-summary.sh +40 -1
  9. package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +37 -0
  10. package/dist/assets/core/core-assets/hooks/session-start-context.sh +79 -2
  11. package/dist/assets/core/core-assets/hooks/stop-verify-reminder.sh +37 -2
  12. package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +97 -2
  13. package/dist/assets/core/core-assets/hooks/worktree-reclaim.sh +39 -0
  14. package/dist/assets/core/core-assets/managed/idioma-rol.md +1 -1
  15. package/dist/assets/core/core-assets/managed/operaciones-seguras.md +13 -1
  16. package/dist/assets/core/core-assets/presets/bun-keystone/managed/stack.md +1 -1
  17. package/dist/assets/core/core-assets/presets/bun-keystone/skills/keystone-models.md +6 -6
  18. package/dist/assets/core/core-assets/presets/bun-keystone/skills/keystone-testing.md +7 -4
  19. package/dist/assets/core/core-assets/presets/bun-keystone/skills/prisma-keystone.md +7 -5
  20. package/dist/assets/core/core-assets/presets/bun-keystone.json +1 -1
  21. package/dist/assets/core/core-assets/settings/settings-base.json +1 -1
  22. package/dist/assets/plugins/codegraph/managed/codegraph-protocol.md +2 -0
  23. package/dist/assets/plugins/codegraph/plugin.json +23 -4
  24. package/dist/assets/plugins/codegraph/skills/codegraph-code-agent.md +27 -0
  25. package/dist/assets/plugins/codegraph/skills/codegraph-rung.md +5 -5
  26. package/dist/assets/plugins/engram/plugin.json +25 -1
  27. package/dist/assets/plugins/engram/skills/engram-subagent.md +25 -0
  28. package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +38 -1
  29. package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +32 -0
  30. package/dist/index.js +386 -354
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -216,6 +216,52 @@ navori registry prune # quitar los que ya no existen
216
216
  Es ortogonal a los workspaces: el registro es "qué repos existen"; el workspace
217
217
  es el perfil de policy (branchBase/prTarget) que cada repo hereda.
218
218
 
219
+ ## Harness global por máquina (opt-in)
220
+
221
+ Todo lo de arriba es por repo. Cuando abres una sesión **fuera** de un repo con navori (un scratch,
222
+ un repo ajeno, tu `~`), no hay harness: ni doctrina de orquestación, ni skills, ni agentes. La capa
223
+ global cubre ese hueco. No la confundas con `render --all` de la sección anterior: aquélla empuja tu
224
+ harness a los repos que **ya** tienen navori; ésta cubre las sesiones que no están en ninguno.
225
+
226
+ Es **opt-in de huella cero**: si nunca corres `navori global init`, no existe `~/.navori/global.json`
227
+ y navori no escribió un solo byte en tu máquina.
228
+
229
+ ```bash
230
+ navori global init # wizard: bloques del baseline + permisos personales.
231
+ # Preview: sin --apply no escribe un solo byte
232
+ navori global init --apply # escribe lo que el preview mostró
233
+ navori global init --recommended --apply # headless (CI): sin preguntas, selección recomendada
234
+ navori global doctor # audita: drift del hook, gate, plugin, permisos y versión
235
+ navori global render --apply # re-renderiza tras un bump del CLI (preview sin --apply)
236
+ navori global uninstall # la retira por completo
237
+ ```
238
+
239
+ El `init` pregunta dos cosas: **qué bloques** componen el baseline (los que declaran `globalSafe`)
240
+ y **qué permisos personales** quieres en `~/.claude/settings.json` — ése es el único camino de UI
241
+ para `permissions`. Re-inicializar **preserva** lo que ya habías elegido: no te resetea a los
242
+ defaults. Sin TTY (CI, pipe) cae solo al camino de `--recommended`, y `--lang es|en` fija el idioma
243
+ del baseline y de los prompts.
244
+
245
+ Qué escribe el `init --apply`, y nada más:
246
+
247
+ - `~/.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
249
+ hook del baseline. Claude Code lo carga sin marketplace ni paso de instalación; las skills globales
250
+ se invocan `/navori:<nombre>` (tras un render, `/reload-plugins` o sesión nueva).
251
+ - `~/.claude/settings.json` — **solo** la clave `permissions`, y solo si declaraste permisos globales
252
+ en el manifest. Con la config por default ni siquiera lo crea.
253
+
254
+ Respeta `CLAUDE_CONFIG_DIR`: si lo tienes seteado, el plugin va ahí en vez de a `~/.claude`.
255
+
256
+ **El baseline se hace a un lado solo.** El hook corre en `SessionStart` y busca un `navori.config.json`
257
+ hacia arriba desde el directorio de la sesión: si lo encuentra, no emite nada — manda el harness del
258
+ repo, que es más específico. Por eso el baseline viaja dentro de un hook y no como bloque estático en
259
+ `~/.claude/CLAUDE.md`: ese archivo se carga siempre y no podría cederle el paso a nadie.
260
+
261
+ `navori global uninstall` retira **solo lo que navori escribió**: el plugin, el manifest y los permisos
262
+ que quedaron registrados como suyos. Un permiso que ya tenías en tu `settings.json` nunca se vuelve de
263
+ navori, así que el uninstall no se lo lleva.
264
+
219
265
  ## Managed blocks con versionado
220
266
 
221
267
  Cada bloque que `navori` inyecta en tu `CLAUDE.md` lleva metadata:
@@ -205,6 +205,14 @@ Never open the PR with the gate red.
205
205
 
206
206
  7. **Checks — read them ONCE, never wait**: `gh pr checks <N> --json name,bucket,state,link,workflow`. `bucket: pending` (the normal case right after creating the PR) → say so in **one extra line** and stop, no retry. `bucket: fail` → name the check in that line and point to `babysit-prs` for the diagnosis. Informative only: you never hold or revert a PR over a red check.
207
207
 
208
+ 8. **Confirm the close actually linked** — only when the body declares one. The body is not evidence of anything; `closingIssuesReferences` is what GitHub parsed out of it:
209
+
210
+ ```bash
211
+ gh pr view <N> --json closingIssuesReferences --jq '[.closingIssuesReferences[].number]'
212
+ ```
213
+
214
+ An empty list next to a `Closes #<N>` in the body means GitHub linked nothing — the keyword was translated, or the number is not an issue of this repo. Report it in **one extra line**, naming the issue that did not link, and stop: rewriting the body of a PR that is already open, and closing the issue by hand, are both the human's call. Informative only, exactly like the checks above.
215
+
208
216
  ## Body template (generic default)
209
217
 
210
218
  ```markdown
@@ -220,9 +228,23 @@ Never open the PR with the gate red.
220
228
  - [ ] `{{qualityGate.full}}` green
221
229
 
222
230
  ## References
223
- - Closes <TICKET-ID> (if applicable, otherwise omit this line)
231
+ - Closes #<N> (an issue of THIS repo; omit the line if there is none)
232
+ - <TICKET-ID> on <tracker> (Jira, Linear, ...; informative, omit if there is none)
224
233
  ```
225
234
 
235
+ **`Closes` is syntax, not prose.** GitHub links and auto-closes an issue only
236
+ when the body carries `Closes` / `Fixes` / `Resolves` followed by `#<N>`, **in
237
+ English**, pointing at an issue of this same repo. The rest of the body follows
238
+ the config's `commits` language and this keyword does NOT: translated (`Cierra
239
+ #<N>`) it is an ordinary sentence, GitHub links nothing, the issue stays open
240
+ and no error says so. That silence is the whole defect — navori's own repo
241
+ shipped 8 PRs that way and closed all 8 issues by hand before anyone noticed
242
+ (#563). Leave the keyword in English even when you translate everything around
243
+ it, and never "fix" it in a later consistency pass.
244
+
245
+ A tracker id (`BT-1427`) is NOT an issue number: GitHub cannot link it, so it
246
+ goes on its own line and never takes a keyword.
247
+
226
248
  If the repo defines its own template (`.github/pull_request_template.md`), read it and match its structure instead of the default.
227
249
 
228
250
  ### Always-on delta — a number in the body, never a gate
@@ -244,7 +266,7 @@ wc -c CLAUDE.md # after
244
266
  - ❌ Never skip hooks (`--no-verify`) unless the user explicitly asks.
245
267
  - ❌ Never ask for a merge / approve the PR yourself. Your job ends with the URL.
246
268
  - ❌ Never `gh pr checks --watch`: it takes no timeout and would hang the turn before the URL reaches the user.
247
- - ✅ Commit and PR message in the language defined by the config's `commits` (`conventional-es` = Spanish MX, `conventional` = English).
269
+ - ✅ Commit and PR message in the language defined by the config's `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).
248
270
  - ✅ 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).
249
271
 
250
272
  ## Anti-patterns
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: leader
3
- description: Do NOT invoke as a subagent. Orchestration playbook that the main agent EMBODIES (see "## Role: orchestrator" in CLAUDE.md). Delegating it to a subagent serializes the work and kills parallelism.
3
+ description: Do NOT invoke as a subagent. Orchestration playbook that the main agent EMBODIES (the "## Role: orchestrator" block, delivered to the session by the SessionStart hook). Delegating it to a subagent serializes the work and kills parallelism.
4
4
  tools: Read, Glob, Grep, Bash, Agent
5
5
  model: {{models.leader}}
6
6
  effort: {{effort.leader}}
@@ -8,7 +8,7 @@ effort: {{effort.leader}}
8
8
 
9
9
  # Orchestrator Playbook (embodied by the main agent)
10
10
 
11
- > This file is a **depth reference** — the orchestrator role **is embodied by the main agent**, not a subagent. The essential mechanics (escalation table, parallelism, synthesis) live inline in the "## Role: orchestrator" block of `CLAUDE.md`, which auto-loads. Here is the extended detail and, below, the **Project rules**. Do NOT invoke `Agent(subagent_type: leader)`.
11
+ > This file is a **depth reference** — the orchestrator role **is embodied by the main agent**, not a subagent. The essential mechanics (escalation table, parallelism, synthesis) live in the "## Role: orchestrator" block, which the `SessionStart` hook delivers to the session — not to a subagent, which is the point: only the main agent can act on it. Here is the extended detail and, below, the **Project rules**. Do NOT invoke `Agent(subagent_type: leader)`.
12
12
 
13
13
  Your only job as orchestrator is to **decompose and coordinate**, never to implement. Note: this applies **when you orchestrate** (R2+ routes of the organic routing). At **R1** (1–3 files, mechanical change or bugfix with a clear cause) you implement **inline yourself**, without opening subagents — see "## Role: orchestrator (organic routing)" in `CLAUDE.md`.
14
14
 
@@ -0,0 +1,179 @@
1
+ # Shared audit-mode event recorder — inlined into each managed hook at render
2
+ # time (see the include directive in the source scripts + lib/hook-includes.ts).
3
+ #
4
+ # WHY (spec 0013): a hook is only visible to the transcript when it BLOCKS or
5
+ # INJECTS context. Every hook that runs and lets the action through is invisible,
6
+ # so `navori audit` could never answer "did the gate run, and what did it cost?".
7
+ # The transcript cannot be fixed — it is the host's format — so the harness
8
+ # records its own execution instead, and the session log becomes the source of
9
+ # truth for what the harness did.
10
+ #
11
+ # Expects `$payload` (the raw hook payload on stdin) to be in scope, and reads
12
+ # `$navori_audit_t0` for the start instant. Both are set by `navori_audit_begin`.
13
+ #
14
+ # Every variable read here carries a `:-` default ON PURPOSE: most managed hooks
15
+ # run under `set -euo pipefail`, where an unset variable ABORTS the hook. A
16
+ # recorder that can abort the thing it observes is worse than no recorder, so the
17
+ # partial must be safe to inline into `set -u` and `set +e` alike.
18
+ #
19
+ # FAIL-OPEN ABSOLUTE, and this matters more here than anywhere else: this code is
20
+ # inlined into hooks whose own contract is to never break a session. Observation
21
+ # must never become the reason an action fails, so every path returns 0 and
22
+ # nothing is ever written to stdout — a stray byte there would be interpreted by
23
+ # the host as hook output (context injection, or a block reason).
24
+
25
+ # Start the clock — but only after establishing that anything will be recorded.
26
+ #
27
+ # THE COST OF BEING OFF is the number that matters here: this code is inlined
28
+ # into hooks that fire on EVERY Bash call, and audit-mode is off for virtually
29
+ # every session of every user. An earlier version ran `perl` plus two `jq`
30
+ # invocations before it ever checked whether a log existed — three processes per
31
+ # hook, four hooks per command, ~48 ms on every single shell call for a feature
32
+ # nobody had turned on.
33
+ #
34
+ # So the gate is a pure-builtin one first: the per-repo audit directory only
35
+ # exists once audit-mode has been activated in this repo at least once. No
36
+ # subprocess, no parsing. Everything expensive lives behind it.
37
+ navori_audit_begin() {
38
+ navori_audit_on=0
39
+
40
+ navori_audit_root=${NAVORI_AUDITS_ROOT:-}
41
+ if [ -z "$navori_audit_root" ]; then
42
+ [ -n "${HOME:-}" ] || return 0
43
+ navori_audit_root=$HOME/.navori/audits
44
+ fi
45
+
46
+ # The gate tests the audit ROOT, not the per-repo directory.
47
+ #
48
+ # Deriving the repo cheaply would mean `${CLAUDE_PROJECT_DIR##*/}` — and that
49
+ # is WRONG: the authoritative repo comes from the payload's `cwd`, and the two
50
+ # differ whenever a hook fires inside an agent worktree, since the hook process
51
+ # starts in the main repo (#454). A gate built on the wrong name would silently
52
+ # record nothing exactly where the harness runs its parallel work.
53
+ #
54
+ # The root alone is enough for what this gate is for: a user who has never
55
+ # activated audit-mode anywhere has no `~/.navori/audits`, so the common case
56
+ # costs one stat and zero processes. Someone who does use audit-mode pays the
57
+ # parsing — which is the cost of the feature they turned on.
58
+ [ -d "$navori_audit_root" ] || return 0
59
+
60
+ navori_audit_on=1
61
+ navori_audit_t0=$(navori_audit_now)
62
+ }
63
+
64
+ # Milliseconds since epoch, spending a process only when it has to.
65
+ #
66
+ # `$EPOCHREALTIME` is a BUILTIN in bash 5 and in zsh (with zsh/datetime, which
67
+ # the harness's zsh path already has): no fork at all. Only a shell without it
68
+ # pays for `perl`, and only a machine without perl degrades to whole seconds —
69
+ # `date` has no portable millisecond format (GNU has %s%3N, BSD does not).
70
+ navori_audit_now() {
71
+ if [ -n "${EPOCHREALTIME:-}" ]; then
72
+ # `1756... .123456` → milliseconds, with pure parameter expansion.
73
+ navori_audit_epoch=${EPOCHREALTIME/,/.}
74
+ printf '%s%s' "${navori_audit_epoch%%.*}" "$(printf '%.3s' "${navori_audit_epoch#*.}")"
75
+ return 0
76
+ fi
77
+ perl -MTime::HiRes=time -e 'printf "%.0f", time*1000' 2>/dev/null \
78
+ || printf '%s' $(( $(date +%s 2>/dev/null || echo 0) * 1000 ))
79
+ }
80
+
81
+ # navori_audit_log <verdict> [reason]
82
+ #
83
+ # `name`, `phase`, `tool` and `source` come from the caller's own variables,
84
+ # which the render sets per hook — the partial never guesses which hook it is
85
+ # inlined into.
86
+ navori_audit_log() {
87
+ # The builtin-only gate from `navori_audit_begin`: when audit-mode was never
88
+ # activated in this repo, nothing below runs and no process is spawned.
89
+ [ "${navori_audit_on:-0}" = "1" ] || return 0
90
+ # No payload, no jq, no clock → nothing to record. Each of these is a normal
91
+ # state for a hook that bailed early, not an error worth surfacing.
92
+ [ -n "${payload:-}" ] || return 0
93
+ command -v jq >/dev/null 2>&1 || return 0
94
+
95
+ # ONE jq for every field, not one per field: this runs on each hook of each
96
+ # Bash call, and a fork is the most expensive thing in it. Newline-separated,
97
+ # read back positionally.
98
+ navori_audit_fields=$(printf '%s' "${payload:-}" | jq -r '[.session_id // "", .cwd // "", .agent_id // .subagent_id // ""] | .[]' 2>/dev/null) || return 0
99
+ navori_audit_session=${navori_audit_fields%%
100
+ *}
101
+ navori_audit_rest=${navori_audit_fields#*
102
+ }
103
+ navori_audit_cwd=${navori_audit_rest%%
104
+ *}
105
+ navori_audit_agent=${navori_audit_rest#*
106
+ }
107
+ [ -n "$navori_audit_session" ] || return 0
108
+ # Same character class the CLI enforces (#503): the id composes a path, so
109
+ # anything path-shaped means the payload is not what we think it is.
110
+ case "$navori_audit_session" in
111
+ *[!A-Za-z0-9_-]*) return 0 ;;
112
+ esac
113
+
114
+ [ -n "$navori_audit_cwd" ] || navori_audit_cwd=$PWD
115
+ navori_audit_repo=$(basename "$navori_audit_cwd" 2>/dev/null) || return 0
116
+ [ -n "$navori_audit_repo" ] || return 0
117
+
118
+ navori_audit_file=$navori_audit_root/$navori_audit_repo/session-$navori_audit_session.log
119
+
120
+ # The session may not be the marked one even in a repo that has been audited
121
+ # before. Also the writability check — a log that cannot be appended to is not
122
+ # an error, it is simply not recording.
123
+ [ -f "$navori_audit_file" ] || return 0
124
+ [ -w "$navori_audit_file" ] || return 0
125
+
126
+ # Volume valve, OFF by default.
127
+ #
128
+ # `PreToolUse(Bash)` chains four hooks, so every shell command leaves four
129
+ # lines and most are `skip` — a long session runs to thousands. Set
130
+ # NAVORI_AUDIT_SKIP_NOOPS=1 to drop the ones that did nothing.
131
+ #
132
+ # Default off on purpose: a `skip` is the ONLY evidence that a hook ran and
133
+ # decided it had no business acting, which is exactly what distinguishes it
134
+ # from a hook that never executed — the question that motivated recording
135
+ # hooks at all. The valve trades that away knowingly; it must not be the
136
+ # silent default.
137
+ if [ "${NAVORI_AUDIT_SKIP_NOOPS:-0}" = "1" ]; then
138
+ case "$1" in
139
+ skip|noop) return 0 ;;
140
+ esac
141
+ fi
142
+
143
+ navori_audit_end=$(navori_audit_now)
144
+ navori_audit_ms=$(( navori_audit_end - ${navori_audit_t0:-$navori_audit_end} ))
145
+ [ "$navori_audit_ms" -ge 0 ] 2>/dev/null || navori_audit_ms=0
146
+
147
+ navori_audit_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) || navori_audit_ts=""
148
+ # `navori_audit_agent` came out of the same single jq above. It is what lets
149
+ # the report attribute a hook to a subagent WITHOUT guessing: with agents
150
+ # running in parallel their time windows overlap, so attribution by timestamp
151
+ # is the fallback, not the primary route.
152
+ #
153
+ # It is recorded, never trusted as an agent identity by itself (#560). What
154
+ # `.agent_id` means depends on the PHASE: on the tool phases it is stable —
155
+ # 485 events of one measured session carried 11 distinct ids, and the
156
+ # subagents' resolved to real transcripts — but on `SubagentStop` the host
157
+ # sends a fresh id per firing: 112 distinct ids for 117 firings, 102 of them
158
+ # matching nothing under `~/.claude`. So a consumer that resolves this field
159
+ # must treat "names nobody" as invalid data rather than as a different agent
160
+ # (`ownerOf` in `lib/audit/parse.ts` is where that rule lives).
161
+
162
+ printf '%s\n' "$(jq -cn \
163
+ --arg ts "$navori_audit_ts" \
164
+ --arg name "${navori_audit_name:-unknown}" \
165
+ --arg phase "${navori_audit_phase:-unknown}" \
166
+ --arg verdict "${1:-unknown}" \
167
+ --arg reason "${2:-}" \
168
+ --arg tool "${navori_audit_tool:-}" \
169
+ --arg src "${navori_audit_source:-core}" \
170
+ --arg agent "${navori_audit_agent:-}" \
171
+ --argjson ms "$navori_audit_ms" \
172
+ '{ts:$ts,event:"hook",name:$name,phase:$phase,verdict:$verdict,ms:$ms,source:$src}
173
+ + (if $tool == "" then {} else {tool:$tool} end)
174
+ + (if $reason == "" then {} else {reason:$reason} end)
175
+ + (if $agent == "" then {} else {agentId:$agent} end)' 2>/dev/null)" \
176
+ >> "$navori_audit_file" 2>/dev/null
177
+
178
+ return 0
179
+ }
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env bash
2
- # navori — audit-mode trigger (UserPromptSubmit)
2
+ # navori — audit-mode prompt recorder (UserPromptSubmit)
3
3
  #
4
- # Detects an audit-mode invocation in the user's prompt and asks Claude to
5
- # CONFIRM with the user before anything is recorded. It never activates the
6
- # mode by itself: a false positive must die in the question, leaving no state
7
- # on disk.
4
+ # While audit-mode is active, appends the typed prompt to the session's
5
+ # append-only log. That is its ONLY job.
8
6
  #
9
- # While the mode is active it appends the typed prompt to the session's
10
- # append-only log. Writes are O_APPEND only; the log is never re-read to be
11
- # rewritten, so parallel subagents cannot corrupt it and a crashed session
12
- # still leaves a valid (merely shorter) file.
7
+ # It used to also detect an audit-mode invocation in the prompt text and ask
8
+ # Claude to confirm activation. That was removed (spec 0013, R3): matching
9
+ # `audit mode` as a substring cannot separate INVOKING the mode from TALKING
10
+ # ABOUT it, and talking about it is what you do all day while working on the
11
+ # feature. The asymmetry made it worse — turning it ON matched loosely, while
12
+ # turning it OFF required the literal phrase, so sessions stayed open forever.
13
+ # Activation is now exclusively `navori audit --start <id>`.
14
+ #
15
+ # Writes are O_APPEND only; the log is never re-read to be rewritten, so
16
+ # parallel subagents cannot corrupt it and a crashed session still leaves a
17
+ # valid (merely shorter) file.
13
18
  #
14
19
  # FAIL-OPEN ABSOLUTE: this hook runs on every prompt. Any error, any missing
15
20
  # dependency, any odd path exits 0 silently. It must never be the reason a
@@ -17,34 +22,6 @@
17
22
 
18
23
  set +e
19
24
 
20
- emit_and_exit() { printf '%s\n' "$1"; exit 0; }
21
-
22
- # Is `audit` actually available in the CLI on PATH?
23
- #
24
- # The hook orders the agent to run `navori audit --start`, and that resolves the
25
- # PUBLISHED binary, never a working tree's build. When the installed version
26
- # predates the subcommand, citty prints the help and exits 0 — so an agent that
27
- # checks the exit code reads a silent no-op as success and reports a recording
28
- # that never started. Match the subcommand inside the CLI's own USAGE line
29
- # instead of trusting the status.
30
- #
31
- # Returns 1 when the subcommand is absent AND when the check itself cannot run
32
- # (no binary on PATH, no USAGE line). Both collapse into "could not confirm",
33
- # which is what the caller's message must say: claiming "your version is old"
34
- # would be wrong for a machine with no navori installed at all.
35
- audit_subcommand_available() {
36
- command -v navori >/dev/null 2>&1 || return 1
37
- usage=$(navori --help 2>/dev/null | grep -m1 '^USAGE' 2>/dev/null) || return 1
38
- [ -n "$usage" ] || return 1
39
- # Normalize separators so the token matches at any position: `USAGE navori
40
- # init|add|audit` -> `|USAGE|navori|init|add|audit|`.
41
- tokens=$(printf '%s' "$usage" | tr ' ' '|' 2>/dev/null) || return 1
42
- case "|$tokens|" in
43
- *"|audit|"*) return 0 ;;
44
- *) return 1 ;;
45
- esac
46
- }
47
-
48
25
  payload=$(cat 2>/dev/null) || exit 0
49
26
  [ -n "$payload" ] || exit 0
50
27
  command -v jq >/dev/null 2>&1 || exit 0
@@ -84,49 +61,20 @@ else
84
61
  fi
85
62
  log_file=$audits_root/$repo/session-$session_id.log
86
63
 
87
- lower=$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]' 2>/dev/null) || exit 0
88
-
89
- # Deliberately tolerant: a spurious match costs one question, while a missed
90
- # one costs the whole recording.
91
- case "$lower" in
92
- *"audit mode"*|*"audit-mode"*|*"modo audit"*|*"modo auditoría"*|*"modo auditoria"*) matched=1 ;;
93
- *) matched=0 ;;
94
- esac
95
-
96
- case "$lower" in
97
- *apaga*|*apagar*|*desactiva*|*"salir de"*|*detén*|*deten*|*stop*|*"turn off"*|*disable*) off_intent=1 ;;
98
- *) off_intent=0 ;;
99
- esac
100
-
101
- if [ -f "$log_file" ]; then
102
- # Active: record the human's own words — they entered the model's context,
103
- # so they cost tokens and belong in the audit.
104
- #
105
- # `transcript_path` rides along because the payload is the ONLY place it is
106
- # stated. Without it the reader has to guess the transcript's location by
107
- # re-deriving Claude Code's undocumented directory encoding (see paths.ts),
108
- # and a guess that misses costs the whole report.
109
- ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) || ts=""
110
- transcript=$(printf '%s' "$payload" | jq -r '.transcript_path // ""' 2>/dev/null) || transcript=""
111
- printf '%s\n' "$(jq -cn --arg ts "$ts" --arg ev "prompt" --arg p "$prompt" --arg tr "$transcript" \
112
- '{ts:$ts,event:$ev,prompt:$p} + (if $tr == "" then {} else {transcript:$tr} end)' 2>/dev/null)" >> "$log_file" 2>/dev/null
64
+ # Not marked not recording. This is also what makes the hook free outside
65
+ # audit-mode: one stat and out.
66
+ [ -f "$log_file" ] || exit 0
113
67
 
114
- if [ "$matched" = "1" ] && [ "$off_intent" = "1" ]; then
115
- if audit_subcommand_available; then
116
- emit_and_exit "[navori audit-mode] The user asked to turn audit-mode OFF. Before doing anything, ask them explicitly: \"audit mode will be turned off, continue?\". Only if they confirm, run: navori audit --stop $session_id (that seals the log and generates the report). Then check the output: it must name the report it wrote. If it prints the command list (USAGE) instead, the installed CLI has no such subcommand — tell the user and do NOT assume the session was closed. If they decline, leave the mode active and carry on with the task."
117
- else
118
- emit_and_exit "[navori audit-mode] The user asked to turn audit-mode OFF, but the available 'navori' could not be confirmed to ship the 'audit' subcommand: it may not be installed at all, or it may predate that command. Do NOT run 'navori audit --stop' blindly: if the binary exists but is old, it prints its help and exits 0, which looks like success without being one. Tell the user what happened and that the session log stays intact on disk, so the report can still be generated once the CLI is up to date. Then carry on with the task."
119
- fi
120
- fi
121
- exit 0
122
- fi
123
-
124
- if [ "$matched" = "1" ] && [ "$off_intent" = "0" ]; then
125
- if audit_subcommand_available; then
126
- emit_and_exit "[navori audit-mode] An audit-mode invocation was detected in the prompt. Before activating anything, ask the user explicitly: \"an audit mode invocation was detected, continue?\". Only if they confirm, run: navori audit --start $session_id (that creates the session log). Then check the output: it must name the log file it created. If it prints the command list (USAGE) instead, the installed CLI has no such subcommand — tell the user and do NOT assume the mode is active. If they decline, run nothing and carry on with the task."
127
- else
128
- emit_and_exit "[navori audit-mode] An audit-mode invocation was detected, but the available 'navori' could not be confirmed to ship the 'audit' subcommand: it may not be installed at all, or it may predate that command. Do NOT run 'navori audit --start' blindly: if the binary exists but is old, it prints its help and exits 0, which looks like success without being one. Tell the user that nothing was activated, and why. Then carry on with the task."
129
- fi
130
- fi
68
+ # Record the human's own words they entered the model's context, so they cost
69
+ # tokens and belong in the audit.
70
+ #
71
+ # `transcript_path` rides along because the payload is the ONLY place it is
72
+ # stated. Without it the reader has to guess the transcript's location by
73
+ # re-deriving Claude Code's undocumented directory encoding (see paths.ts),
74
+ # and a guess that misses costs the whole report.
75
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) || ts=""
76
+ transcript=$(printf '%s' "$payload" | jq -r '.transcript_path // ""' 2>/dev/null) || transcript=""
77
+ printf '%s\n' "$(jq -cn --arg ts "$ts" --arg ev "prompt" --arg p "$prompt" --arg tr "$transcript" \
78
+ '{ts:$ts,event:$ev,prompt:$p} + (if $tr == "" then {} else {transcript:$tr} end)' 2>/dev/null)" >> "$log_file" 2>/dev/null
131
79
 
132
80
  exit 0
@@ -30,7 +30,43 @@ set -euo pipefail
30
30
  # A missing JSON parser used to make this guard wave every command through; the
31
31
  # shared extractor falls back to sed so it still inspects the command.
32
32
  # navori:include extract-cmd
33
- [ -z "$cmd" ] && exit 0
33
+
34
+ navori_audit_name="guard-destructive"
35
+ navori_audit_phase="PreToolUse"
36
+ navori_audit_tool="Bash"
37
+ # Fallback no-ops, overwritten by the real definitions the include brings in.
38
+ # They exist because this hook is FAIL-OPEN: if the file ever runs WITHOUT its
39
+ # includes expanded — a raw copy of the asset, a render that half-finished — an
40
+ # undefined function would be exit 127, and under `set -e` that KILLS the hook.
41
+ # A recorder that can kill the thing it observes is the one bug this partial may
42
+ # never have.
43
+ navori_audit_begin() { :; }
44
+ navori_audit_log() { :; }
45
+ # navori:include audit-log
46
+ navori_audit_begin
47
+
48
+ # The verdict is resolved in a trap, NOT by a call at the end of the file.
49
+ #
50
+ # This hook's managed block ends before its `navori:user-section`, and the render
51
+ # only syncs what is INSIDE the block — so a call placed after it lives in the
52
+ # user's own territory and never reaches the mirror. That is exactly how the most
53
+ # critical hook in the harness ended up being the only one not recording.
54
+ #
55
+ # The trap also covers the extra guards a user writes in that section: whatever
56
+ # they add, the exit code still tells the truth about what happened.
57
+ navori_audit_verdict="allow"
58
+ navori_audit_reason=""
59
+ navori_audit_on_exit() {
60
+ navori_audit_log "$navori_audit_verdict" "$navori_audit_reason" || true
61
+ return 0
62
+ }
63
+ trap navori_audit_on_exit EXIT
64
+
65
+ if [ -z "$cmd" ]; then
66
+ navori_audit_verdict="skip"
67
+ navori_audit_reason="sin comando que inspeccionar"
68
+ exit 0
69
+ fi
34
70
 
35
71
  # branchBase is shell-quoted at render time via the shq: marker (#197): a
36
72
  # hostile branchBase in navori.config.json lands here as an inert literal,
@@ -43,6 +79,11 @@ block() {
43
79
  # should carry, and the reason line already says which rule fired.
44
80
  echo "[navori] command: ${cmd:0:2000}" >&2
45
81
  echo "[navori] if intentional, run the command yourself outside the agent." >&2
82
+ # Only ASSIGNMENTS here: this is the one place the guard says no, and nothing
83
+ # may come between the decision and `exit 2`. The recording happens in the
84
+ # trap, after the exit is already committed.
85
+ navori_audit_verdict="block"
86
+ navori_audit_reason="$1"
46
87
  exit 2
47
88
  }
48
89
 
@@ -40,6 +40,46 @@
40
40
  # in a normal session is none and in a bad one is one.
41
41
  set -uo pipefail
42
42
 
43
+ # PostToolUse delivers its payload on stdin; this hook never needed it and the
44
+ # audit recorder does (session_id/cwd), so it is captured rather than ignored.
45
+ payload=$(cat 2>/dev/null) || payload=""
46
+
47
+ navori_audit_name="managed-drift-watch"
48
+ navori_audit_phase="PostToolUse"
49
+ navori_audit_tool="Bash"
50
+ # Fallback no-ops, overwritten by the real definitions the include brings in.
51
+ # They exist because this hook is FAIL-OPEN: if the file ever runs WITHOUT its
52
+ # includes expanded — a raw copy of the asset, a render that half-finished — an
53
+ # undefined function would be exit 127, and under `set -e` that KILLS the hook.
54
+ # A recorder that can kill the thing it observes is the one bug this partial may
55
+ # never have.
56
+ navori_audit_begin() { :; }
57
+ navori_audit_log() { :; }
58
+ # navori:include audit-log
59
+ navori_audit_begin
60
+
61
+ # This hook has SEVEN early exits (no sha, no roots, no changes, …). Wiring a
62
+ # call into each is how the set drifts the next time one is added, so the verdict
63
+ # is derived once, from the exit code, in a trap.
64
+ #
65
+ # `navori_audit_reached_check` is what separates "ran and found nothing" from
66
+ # "bailed before checking anything" — the distinction the whole exercise exists
67
+ # for. Without it every early exit would report `clean`, i.e. would claim a
68
+ # verification that never happened.
69
+ navori_audit_reached_check=0
70
+ navori_audit_on_exit() {
71
+ navori_audit_code=$?
72
+ if [ "$navori_audit_code" -eq 2 ]; then
73
+ navori_audit_log "dirty" "bloque managed con hash desalineado"
74
+ elif [ "$navori_audit_reached_check" -eq 1 ]; then
75
+ navori_audit_log "clean"
76
+ else
77
+ navori_audit_log "skip" "sin nada que verificar"
78
+ fi
79
+ return 0
80
+ }
81
+ trap navori_audit_on_exit EXIT
82
+
43
83
  cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0
44
84
 
45
85
  stamp=".claude/.managed-drift-stamp"
@@ -121,6 +161,7 @@ done <<EOF
121
161
  $changed
122
162
  EOF
123
163
 
164
+ navori_audit_reached_check=1
124
165
  [ -z "$drift" ] && exit 0
125
166
 
126
167
  # Exit 2 so the text reaches the model rather than scrolling past in a log: the
@@ -21,14 +21,53 @@
21
21
  # The managed block is regenerated by `navori render`; do NOT edit by hand.
22
22
  set -euo pipefail
23
23
 
24
- cat >/dev/null 2>&1 || true # drain the PreCompact JSON on stdin (unused)
24
+ # The PreCompact JSON was drained and discarded here; it is kept now because the
25
+ # audit recorder reads `session_id`/`cwd` out of it. Draining is still the point:
26
+ # an undrained stdin can leave the host writing into a closed pipe.
27
+ payload=$(cat 2>/dev/null) || payload=""
28
+
29
+ navori_audit_name="precompact-session-summary"
30
+ navori_audit_phase="PreCompact"
31
+ # Fallback no-ops, overwritten by the real definitions the include brings in.
32
+ # They exist because this hook is FAIL-OPEN: if the file ever runs WITHOUT its
33
+ # includes expanded — a raw copy of the asset, a render that half-finished — an
34
+ # undefined function would be exit 127, and under `set -e` that KILLS the hook.
35
+ # A recorder that can kill the thing it observes is the one bug this partial may
36
+ # never have.
37
+ navori_audit_begin() { :; }
38
+ navori_audit_log() { :; }
39
+ # navori:include audit-log
40
+ navori_audit_begin
41
+
42
+ # The verdict is a VARIABLE resolved in a trap, not a call per branch. These
43
+ # hooks have several early exits each (no git, no worktrees, nothing to inject),
44
+ # and wiring a call into every one is how the set drifts the next time somebody
45
+ # adds an exit. Defaulting to `skip` makes a new early exit semantically correct
46
+ # for free: it means "ran, decided it had nothing to do", which is exactly what
47
+ # an unhandled early return is.
48
+ navori_audit_verdict="skip"
49
+ navori_audit_reason=""
50
+ navori_audit_on_exit() {
51
+ navori_audit_log "$navori_audit_verdict" "$navori_audit_reason" || true
52
+ return 0
53
+ }
54
+ trap navori_audit_on_exit EXIT
55
+
25
56
 
26
57
  ctx="A punto de compactar el contexto: el detalle turno-a-turno se resumirá. Antes de perderlo, persiste un resumen de sesión con tu herramienta de memoria (engram: guarda el resumen de sesión) y/o anota las decisiones y los bugs con causa raíz de esta sesión en progress/current.md."
27
58
 
28
59
  # Emit additionalContext safely: node (best escaping) → jq → give up (exit 0).
29
60
  if command -v node >/dev/null 2>&1; then
30
61
  CTX="$ctx" node -e 'process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"PreCompact",additionalContext:process.env.CTX}}))'
62
+ navori_audit_verdict="inject"
31
63
  elif command -v jq >/dev/null 2>&1; then
32
64
  jq -n --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreCompact",additionalContext:$ctx}}'
65
+ navori_audit_verdict="inject"
66
+ else
67
+ # Neither serializer available: the reminder never reached the model. Recorded
68
+ # as `noop` rather than `inject`, because the audit must not claim a context
69
+ # injection that did not happen.
70
+ navori_audit_verdict="noop"
71
+ navori_audit_reason="sin node ni jq: el recordatorio no se emitio"
33
72
  fi
34
73
  exit 0
@@ -20,6 +20,42 @@ set -euo pipefail
20
20
  # Command extraction (payload → $cmd). Shared body, single source of truth.
21
21
  # navori:include extract-cmd
22
22
 
23
+ navori_audit_name="quality-gate-pre-commit"
24
+ navori_audit_phase="PreToolUse"
25
+ navori_audit_tool="Bash"
26
+ # Fallback no-ops, overwritten by the real definitions the include brings in.
27
+ # They exist because this hook is FAIL-OPEN: if the file ever runs WITHOUT its
28
+ # includes expanded — a raw copy of the asset, a render that half-finished — an
29
+ # undefined function would be exit 127, and under `set -e` that KILLS the hook.
30
+ # A recorder that can kill the thing it observes is the one bug this partial may
31
+ # never have.
32
+ navori_audit_begin() { :; }
33
+ navori_audit_log() { :; }
34
+ # navori:include audit-log
35
+ navori_audit_begin
36
+
37
+ # This hook fires on EVERY Bash call and does real work on almost none of them,
38
+ # so its verdict is derived from the exit code in a trap rather than from a call
39
+ # per branch — the branch set here is the one most likely to grow.
40
+ #
41
+ # `navori_audit_ran_gate` is the distinction that matters: the recorded `ms` of a
42
+ # run that actually executed the gate (tens of seconds) means something very
43
+ # different from that of a run that looked at the command and moved on (single
44
+ # digits). Collapsing both into `allow` would make the timing unreadable.
45
+ navori_audit_ran_gate=0
46
+ navori_audit_on_exit() {
47
+ navori_audit_code=$?
48
+ if [ "$navori_audit_code" -ne 0 ]; then
49
+ navori_audit_log "block" "el quality gate no paso o no pudo correr" || true
50
+ elif [ "$navori_audit_ran_gate" -eq 1 ]; then
51
+ navori_audit_log "allow" "gate ejecutado y verde" || true
52
+ else
53
+ navori_audit_log "skip" "el comando no es un commit" || true
54
+ fi
55
+ return 0
56
+ }
57
+ trap navori_audit_on_exit EXIT
58
+
23
59
  # Detect the project's REAL package manager from lockfiles / package.json, so a
24
60
  # gate command hardcoded to one PM (e.g. `pnpm run ...`) can still run in a repo
25
61
  # that actually uses another (e.g. bun). Mirrors lib/detect.ts precedence:
@@ -45,6 +81,7 @@ is_pm() {
45
81
  }
46
82
 
47
83
  run_gate() {
84
+ navori_audit_ran_gate=1
48
85
  echo "[navori] running quality-gate fast: $1" >&2
49
86
  eval "$1" || {
50
87
  echo "[navori] quality-gate fast failed. Commit aborted." >&2