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.
- package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +170 -0
- package/dist/assets/core/core-assets/hooks/audit-mode-trigger.sh +28 -80
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +42 -1
- package/dist/assets/core/core-assets/hooks/managed-drift-watch.sh +41 -0
- package/dist/assets/core/core-assets/hooks/precompact-session-summary.sh +40 -1
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +37 -0
- package/dist/assets/core/core-assets/hooks/session-start-context.sh +46 -2
- package/dist/assets/core/core-assets/hooks/stop-verify-reminder.sh +37 -2
- package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +41 -2
- package/dist/assets/core/core-assets/hooks/worktree-reclaim.sh +39 -0
- package/dist/assets/core/core-assets/settings/settings-base.json +1 -1
- package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +38 -1
- package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +32 -0
- package/dist/index.js +351 -333
- package/package.json +1 -1
|
@@ -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
|
|
2
|
+
# navori — audit-mode prompt recorder (UserPromptSubmit)
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[ -
|
|
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
|
-
|
|
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
|
-
|
|
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
|