navori 0.6.4 → 0.6.5

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.
@@ -0,0 +1,170 @@
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
+ printf '%s\n' "$(jq -cn \
154
+ --arg ts "$navori_audit_ts" \
155
+ --arg name "${navori_audit_name:-unknown}" \
156
+ --arg phase "${navori_audit_phase:-unknown}" \
157
+ --arg verdict "${1:-unknown}" \
158
+ --arg reason "${2:-}" \
159
+ --arg tool "${navori_audit_tool:-}" \
160
+ --arg src "${navori_audit_source:-core}" \
161
+ --arg agent "${navori_audit_agent:-}" \
162
+ --argjson ms "$navori_audit_ms" \
163
+ '{ts:$ts,event:"hook",name:$name,phase:$phase,verdict:$verdict,ms:$ms,source:$src}
164
+ + (if $tool == "" then {} else {tool:$tool} end)
165
+ + (if $reason == "" then {} else {reason:$reason} end)
166
+ + (if $agent == "" then {} else {agentId:$agent} end)' 2>/dev/null)" \
167
+ >> "$navori_audit_file" 2>/dev/null
168
+
169
+ return 0
170
+ }
@@ -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
@@ -22,7 +22,38 @@
22
22
  # The `{{...}}` placeholders are filled by `navori render`; do NOT edit by hand.
23
23
  set -euo pipefail
24
24
 
25
- cat >/dev/null 2>&1 || true # drain the SessionStart JSON on stdin (unused)
25
+ # The payload was drained and discarded here; it is kept now because the audit
26
+ # recorder reads `session_id`/`cwd` out of it. Draining is still the point: an
27
+ # undrained stdin can leave the host writing into a closed pipe.
28
+ payload=$(cat 2>/dev/null) || payload=""
29
+
30
+ navori_audit_name="session-start-context"
31
+ navori_audit_phase="SessionStart"
32
+ # Fallback no-ops, overwritten by the real definitions the include brings in.
33
+ # They exist because this hook is FAIL-OPEN: if the file ever runs WITHOUT its
34
+ # includes expanded — a raw copy of the asset, a render that half-finished — an
35
+ # undefined function would be exit 127, and under `set -e` that KILLS the hook.
36
+ # A recorder that can kill the thing it observes is the one bug this partial may
37
+ # never have.
38
+ navori_audit_begin() { :; }
39
+ navori_audit_log() { :; }
40
+ # navori:include audit-log
41
+ navori_audit_begin
42
+
43
+ # The verdict is a VARIABLE resolved in a trap, not a call per branch. These
44
+ # hooks have several early exits each (no git, no worktrees, nothing to inject),
45
+ # and wiring a call into every one is how the set drifts the next time somebody
46
+ # adds an exit. Defaulting to `skip` makes a new early exit semantically correct
47
+ # for free: it means "ran, decided it had nothing to do", which is exactly what
48
+ # an unhandled early return is.
49
+ navori_audit_verdict="skip"
50
+ navori_audit_reason=""
51
+ navori_audit_on_exit() {
52
+ navori_audit_log "$navori_audit_verdict" "$navori_audit_reason" || true
53
+ return 0
54
+ }
55
+ trap navori_audit_on_exit EXIT
56
+
26
57
 
27
58
  ctx=""
28
59
  add() { ctx="${ctx}${1}"$'\n'; }
@@ -103,12 +134,25 @@ if [ -d "$HOME/.navori/workspaces" ] && command -v navori >/dev/null 2>&1; then
103
134
  fi
104
135
  fi
105
136
 
106
- [ -n "$ctx" ] || exit 0
137
+ if [ -z "$ctx" ]; then
138
+ navori_audit_verdict="noop"
139
+ navori_audit_reason="no habia contexto que inyectar"
140
+ exit 0
141
+ fi
107
142
 
108
143
  # Emit the JSON safely: node (best escaping) → jq → give up (exit 0, no context).
109
144
  if command -v node >/dev/null 2>&1; then
110
145
  CTX="$ctx" node -e 'process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:process.env.CTX}}))'
146
+ # `bytes` is what makes this measurable: session startup is the single largest
147
+ # context cost of a session, and this hook is one of its inputs.
148
+ navori_audit_verdict="inject"
149
+ navori_audit_reason="${#ctx} bytes"
111
150
  elif command -v jq >/dev/null 2>&1; then
112
151
  jq -n --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$ctx}}'
152
+ navori_audit_verdict="inject"
153
+ navori_audit_reason="${#ctx} bytes"
154
+ else
155
+ navori_audit_verdict="noop"
156
+ navori_audit_reason="sin node ni jq: el contexto no se emitio"
113
157
  fi
114
158
  exit 0
@@ -21,7 +21,38 @@
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 Stop JSON on stdin (unused)
24
+ # The payload was drained and discarded here; it is kept now because the audit
25
+ # recorder reads `session_id`/`cwd` out of it. Draining is still the point: an
26
+ # undrained stdin can leave the host writing into a closed pipe.
27
+ payload=$(cat 2>/dev/null) || payload=""
28
+
29
+ navori_audit_name="stop-verify-reminder"
30
+ navori_audit_phase="Stop"
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
  # Only meaningful inside a git repo.
27
58
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -30,7 +61,9 @@ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
30
61
  # (staged or unstaged). Untracked files alone don't count — scratch/output files
31
62
  # are common and shouldn't trigger the nudge.
32
63
  if git diff --quiet --ignore-submodules HEAD 2>/dev/null; then
33
- exit 0 # working tree clean vs HEAD → nothing to remind about
64
+ navori_audit_verdict="noop"
65
+ navori_audit_reason="arbol limpio: nada que recordar"
66
+ exit 0
34
67
  fi
35
68
 
36
69
  msg="navori: cambios sin commitear en el árbol. Antes de dar la tarea por terminada, corre el quality gate (evidencia fresca este turno) y haz commit — verify-before-done (P4)."
@@ -42,4 +75,6 @@ if command -v node >/dev/null 2>&1; then
42
75
  elif command -v jq >/dev/null 2>&1; then
43
76
  jq -n --arg m "$msg" '{systemMessage:$m}'
44
77
  fi
78
+ navori_audit_verdict="inject"
79
+ navori_audit_reason="cambios sin commitear"
45
80
  exit 0