navori 0.8.4 → 0.8.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.
@@ -36,6 +36,11 @@
36
36
  "src": "scripts/tgrep-session.sh",
37
37
  "dest": "tgrep-session.sh",
38
38
  "exec": true
39
+ },
40
+ {
41
+ "src": "scripts/guard-search-routing.sh",
42
+ "dest": "guard-search-routing.sh",
43
+ "exec": true
39
44
  }
40
45
  ],
41
46
  "hooks": [
@@ -44,6 +49,13 @@
44
49
  "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/scripts/tgrep-session.sh\"",
45
50
  "timeout": 30,
46
51
  "statusMessage": "navori/tgrep: search index"
52
+ },
53
+ {
54
+ "event": "PreToolUse",
55
+ "matcher": "Bash",
56
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/scripts/guard-search-routing.sh\"",
57
+ "timeout": 10,
58
+ "statusMessage": "navori/tgrep: routing guard"
47
59
  }
48
60
  ],
49
61
  "skills": [
@@ -77,5 +89,7 @@
77
89
  "injectInto": ".claude/agents/reviewer.md"
78
90
  }
79
91
  ],
80
- "invariants": ["tgrep-search.sh"]
92
+ "invariants": [
93
+ "tgrep-search.sh"
94
+ ]
81
95
  }
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PreToolUse(Bash) guard: content search goes through the tgrep wrapper.
4
+ #
5
+ # WHY THIS IS A GUARD AND NOT A SENTENCE IN CLAUDE.md. The doctrine telling the
6
+ # model to route content search through the wrapper has shipped since 0.7.8 and
7
+ # is measured at 7.4% adoption over 2,761 real searches. The session that
8
+ # produced the best analysis of tgrep anyone has written still went through
9
+ # shell for 9 of every 10 searches (#668), so comprehension was never the
10
+ # bottleneck. In this harness the mechanical layers hold — guard-destructive 14
11
+ # blocks out of 14, quality-gate-pre-commit 7 out of 7 — and the advisory ones
12
+ # do not. This moves one doctrine across that line.
13
+ #
14
+ # WHAT IT BLOCKS, and nothing else: a segment that STARTS a command (never one
15
+ # after a pipe) and invokes `grep`/`egrep`/`fgrep` recursively, or `rg` with no
16
+ # single file target. Measured against the 8,562 real grep/rg invocations of the
17
+ # park: it fires on 55% of genuine content searches and on ZERO extractions.
18
+ #
19
+ # WHAT IT DELIBERATELY LETS THROUGH, because a false block teaches the model to
20
+ # route AROUND the guard — which is worse than the search it would have stopped:
21
+ #
22
+ # · `… | grep x` filtering another command's output (46% of all
23
+ # invocations). Not a repo search; the wrapper cannot
24
+ # replace it.
25
+ # · `grep -n x file.ts` extracting from an ALREADY KNOWN file (22%). The
26
+ # harness's own doctrine prefers this to the wrapper.
27
+ # · anything it cannot parse with certainty — a quoted pattern carrying a
28
+ # newline, an unbalanced quote. Unsure means allow.
29
+ #
30
+ # The managed block below is regenerated by `navori render` and must NOT be
31
+ # edited by hand. Add repo-specific exceptions in the user section at the
32
+ # bottom — `$cmd` is already parsed and in scope there.
33
+ set -euo pipefail
34
+
35
+ # Command extraction (payload → $cmd). Shared body, single source of truth.
36
+ # navori:include extract-cmd
37
+ cmd=$(extract_cmd)
38
+
39
+ navori_audit_name="guard-search-routing"
40
+ navori_audit_phase="PreToolUse"
41
+ navori_audit_tool="Bash"
42
+ # `source` names the PLUGIN, not core: disabling tgrep changes which hooks run,
43
+ # and without this the report cannot explain why a phase thinned out between two
44
+ # sessions.
45
+ navori_audit_source="plugin:tgrep"
46
+ # Fallback no-ops, overwritten by the real definitions the include brings in.
47
+ # They exist because this hook is FAIL-OPEN: if the file ever runs WITHOUT its
48
+ # includes expanded — a raw copy of the asset, a render that half-finished — an
49
+ # undefined function would be exit 127, and under `set -e` that KILLS the hook.
50
+ navori_audit_begin() { :; }
51
+ navori_audit_log() { :; }
52
+ # navori:include audit-log
53
+ navori_audit_begin
54
+
55
+ # The verdict is resolved in a trap, not by a call at the end of the file: this
56
+ # hook's managed block ends before its user section, so a call placed after it
57
+ # would live in the user's territory and never reach the mirror.
58
+ navori_audit_verdict="allow"
59
+ navori_audit_reason=""
60
+ navori_audit_on_exit() {
61
+ navori_audit_log "$navori_audit_verdict" "$navori_audit_reason" || true
62
+ return 0
63
+ }
64
+ trap navori_audit_on_exit EXIT
65
+
66
+ if [ -z "$cmd" ]; then
67
+ navori_audit_verdict="skip"
68
+ navori_audit_reason="sin comando que inspeccionar"
69
+ exit 0
70
+ fi
71
+
72
+ # The wrapper itself is never blocked — it runs `grep -rn` internally on the
73
+ # fallback path, and this hook sees the command the agent typed, not the one the
74
+ # wrapper spawns. Checked FIRST so no rule below can ever fire on it.
75
+ case "$cmd" in
76
+ *tgrep-search.sh*)
77
+ navori_audit_verdict="skip"
78
+ navori_audit_reason="es el wrapper"
79
+ exit 0
80
+ ;;
81
+ esac
82
+
83
+ # BOUNDED WORK: the guard runs under a wall-clock timeout it does not control,
84
+ # and being killed is indistinguishable from approving. Every pass below is one
85
+ # linear `sed`/`grep` over the command; a command past the ceiling is ALLOWED
86
+ # rather than inspected, because this guard redirects a search — it does not
87
+ # protect against data loss, so failing open is the right failure here (and the
88
+ # opposite of what guard-destructive does with the same trade-off).
89
+ if [ "${#cmd}" -gt 20000 ]; then
90
+ navori_audit_verdict="skip"
91
+ navori_audit_reason="comando demasiado grande para inspeccionar"
92
+ exit 0
93
+ fi
94
+
95
+ # SEGMENTS. A command is split so each rule only ever matches WITHIN one
96
+ # segment, and each segment carries whether it starts a command or continues a
97
+ # pipe. That distinction is the whole design: `grep -n foo` is extraction from
98
+ # stdin after a `|`, and a repo search at the start of a line. The segment alone
99
+ # cannot tell them apart.
100
+ #
101
+ # Order matters: `||` must be rewritten before `|`, or the first `|` of a `||`
102
+ # is consumed and the second starts a bogus "piped" segment.
103
+ #
104
+ # A line continuation is NOT a separator: without joining it first, `\` + newline
105
+ # splits a quoted pattern in half and the fragment lands with no target, which
106
+ # every heuristic below reads as a recursive search. Measured on the park: that
107
+ # alone mislabelled over a thousand extractions.
108
+ segments=$(printf '%s' "$cmd" \
109
+ | sed -e ':a' -e '$!N' -e 's/\\\n/ /' -e 'ta' -e 'P' -e 'D' \
110
+ | sed -e 's/||/\
111
+ @C@/g' -e 's/&&/\
112
+ @C@/g' -e 's/;/\
113
+ @C@/g' -e 's/|/\
114
+ @P@/g')
115
+
116
+ block() {
117
+ echo "[navori] BLOCKED by guard-search-routing: $1" >&2
118
+ echo "[navori] route content search through the indexed wrapper:" >&2
119
+ echo "[navori] bash .claude/scripts/tgrep-search.sh <same ripgrep flags>" >&2
120
+ echo "[navori] it carries its own 'allow' rule: no prompt, no classifier round-trip." >&2
121
+ echo "[navori] exit contract: 0 = match, 1 = no match, 2 = NOTHING WAS SEARCHED." >&2
122
+ echo "[navori] this does NOT apply to '| grep' (filtering output) nor to" >&2
123
+ echo "[navori] 'grep -n x known-file' (extracting from a file you already found):" >&2
124
+ echo "[navori] both of those stay correct, and the wrapper cannot replace them." >&2
125
+ # Only assignments here: nothing may come between the decision and `exit 2`.
126
+ # The recording happens in the trap, after the exit is already committed.
127
+ navori_audit_verdict="block"
128
+ navori_audit_reason="$1"
129
+ exit 2
130
+ }
131
+
132
+ # Walk the command-start segments only. `@P@` lines are dropped here, and that
133
+ # is what keeps every pipe-filter out of every rule below: the first line of the
134
+ # command carries no marker (it starts the command), and each `@C@` line starts
135
+ # a new one.
136
+ starts=$(printf '%s\n' "$segments" | sed -n -e 's/^@C@//p' -e '1{/^@[CP]@/!p;}')
137
+
138
+ while IFS= read -r seg; do
139
+ [ -n "$seg" ] || continue
140
+
141
+ # Recursive grep: the flag may sit anywhere among the options, so the verb is
142
+ # anchored at the segment start and the flag matched as its own token.
143
+ # `-[a-zA-Z]*[rR][a-zA-Z]*` matches `-r`, `-R`, `-rn`, `-nr`; it cannot match
144
+ # `--include` or `--color`, because after the leading `-` the class does not
145
+ # accept another `-`.
146
+ if printf '%s' "$seg" | grep -qE '^[[:space:]]*(grep|egrep|fgrep)([[:space:]]|$)' \
147
+ && printf '%s' "$seg" | grep -qE '(^|[[:space:]])(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)([[:space:]]|=|$)'; then
148
+ block "busqueda de contenido recursiva por shell"
149
+ fi
150
+
151
+ # `rg` is recursive by DEFAULT, so the question inverts: it is extraction only
152
+ # when a single concrete file is named. The test is deliberately loose — any
153
+ # token that looks like a path with an extension — because the cost of the two
154
+ # errors is not symmetric. Missing a search costs one unmeasured call; blocking
155
+ # a legitimate extraction teaches the model to work around the guard.
156
+ if printf '%s' "$seg" | grep -qE '^[[:space:]]*rg([[:space:]]|$)' \
157
+ && ! printf '%s' "$seg" | grep -qE '(^|[[:space:]])[^[:space:]]*\.[A-Za-z0-9]+([[:space:]]|$)'; then
158
+ block "busqueda de contenido por shell (rg es recursivo por defecto)"
159
+ fi
160
+ done <<EOF
161
+ $starts
162
+ EOF
163
+
164
+ # navori:user-section
165
+ # user: add repo-specific exceptions or extra redirects here. `$cmd` holds the
166
+ # full command and `block "<reason>"` aborts with exit 2.
167
+
168
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navori",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "Multi-agent harness + SDD scaffolder for Claude Code and other AI engines",
5
5
  "type": "module",
6
6
  "bin": {