@neat.is/claude-skill 0.9.2-dev.20260821 → 0.9.2
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/GRAPH_FIRST.md +34 -20
- package/hooks/neat-search-nudge.mjs +110 -8
- package/package.json +1 -1
package/GRAPH_FIRST.md
CHANGED
|
@@ -1,35 +1,49 @@
|
|
|
1
|
-
<!-- NEAT graph-first
|
|
1
|
+
<!-- NEAT graph-first directive. Paste this block into your agent's project
|
|
2
2
|
instructions — CLAUDE.md, AGENTS.md, .cursorrules, or the equivalent —
|
|
3
|
-
so the agent
|
|
3
|
+
so the agent queries NEAT's graph before it scans files by hand.
|
|
4
4
|
Agent-agnostic: it is plain guidance, no Claude Code features required. -->
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Query the graph FIRST
|
|
7
7
|
|
|
8
|
-
This project has NEAT wired in
|
|
9
|
-
code, infrastructure, and runtime behaviour
|
|
10
|
-
|
|
11
|
-
`
|
|
12
|
-
|
|
8
|
+
This project has NEAT wired in: a live, fused semantic graph of the system —
|
|
9
|
+
code, infrastructure, and runtime behaviour (OpenTelemetry) in one model. Every
|
|
10
|
+
fact carries provenance — `EXTRACTED` from source, `OBSERVED` from OTel,
|
|
11
|
+
`INFERRED` where the trace stitcher bridges a gap, `STALE` when runtime goes
|
|
12
|
+
quiet — plus a confidence, so you know how much to trust each answer.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
**For ANY question about this system's behaviour, dependencies, failures, root
|
|
15
|
+
cause, or blast radius, call `neat ask` FIRST — before Read, Grep, Glob, or
|
|
16
|
+
Bash.** You do not need to know which tool or the exact node id: `ask` is the
|
|
17
|
+
front door. It resolves the entities in your question to graph nodes and routes
|
|
18
|
+
it to the right traversal, returning one compact, provenance-tagged answer.
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
```
|
|
21
|
+
neat ask "why is checkout failing?"
|
|
22
|
+
neat ask "what breaks if I change the orders table?"
|
|
23
|
+
neat ask "what does the payments service depend on at runtime?"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Same door over MCP: the `ask` tool (`mcp__neat__ask`). Reach for it first.
|
|
27
|
+
|
|
28
|
+
The graph is live and fused: it is faster and more accurate than
|
|
29
|
+
`grep`/`glob`/`find`, and it can tell you what the system *actually does at
|
|
30
|
+
runtime*, not only what the source declares.
|
|
31
|
+
|
|
32
|
+
When you already have a node id and want one specific traversal, the structured
|
|
33
|
+
tools answer directly:
|
|
19
34
|
|
|
20
35
|
- `semantic_search` — find code/nodes by a natural-language description.
|
|
21
|
-
- `get_dependencies` — a node's transitive outgoing dependencies (
|
|
22
|
-
- `get_observed_dependencies` — only what a node calls *in production* (`OBSERVED`
|
|
36
|
+
- `get_dependencies` — a node's transitive outgoing dependencies (`EXTRACTED`).
|
|
37
|
+
- `get_observed_dependencies` — only what a node calls *in production* (`OBSERVED`).
|
|
23
38
|
- `get_divergences` — where the code (`EXTRACTED`) and production (`OBSERVED`) disagree.
|
|
24
|
-
The most NEAT-shaped query; reach for it on "is anything weird here?"
|
|
25
39
|
- `get_root_cause` — trace a failing node up its dependency graph to the culprit.
|
|
26
|
-
- `get_blast_radius` — everything downstream
|
|
40
|
+
- `get_blast_radius` — everything downstream: what breaks if a node changes or fails.
|
|
27
41
|
- `get_incident_history` — recent OTel error events recorded against a node.
|
|
28
42
|
- `check_policies` — the project's `policy.json` violations, actual or hypothetical.
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
comments, string literals, config minutiae, a file NEAT does not model
|
|
32
|
-
|
|
44
|
+
Fall back to text search only when the graph does not have what you need —
|
|
45
|
+
comments, string literals, config minutiae, a file NEAT does not model. The rule
|
|
46
|
+
is order: ask the graph first, then scan.
|
|
33
47
|
|
|
34
|
-
If the tools are not available, the NEAT daemon may not be running (`
|
|
48
|
+
If the tools are not available, the NEAT daemon may not be running (`neat list`)
|
|
35
49
|
or the MCP server may not be wired in (`neat skill --apply`).
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// NEAT search
|
|
2
|
+
// NEAT search hook — a Claude Code PreToolUse hook with two modes.
|
|
3
3
|
//
|
|
4
4
|
// When the agent reaches for raw text search — the Grep or Glob tools, or a
|
|
5
5
|
// Bash command that shells out to grep / rg / ag / ack / find / fd — this hook
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// steers it to NEAT's graph first. NEAT already holds this project's structure,
|
|
7
|
+
// dependencies, and runtime behaviour, so a graph query is usually faster and
|
|
8
|
+
// more accurate than scanning files by hand.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// NUDGE (default) — injects a short "ask the graph first" note and always lets
|
|
11
|
+
// the search run. It never denies; anything that is not a text search is a
|
|
12
|
+
// silent no-op.
|
|
13
|
+
//
|
|
14
|
+
// GATE (opt-in) — DENIES Grep / Glob / grep-family Bash until `neat ask` (the
|
|
15
|
+
// `mcp__neat__ask` tool or a Bash `neat ask …`) has run at least once this
|
|
16
|
+
// session, forcing the graph-first orientation. Once the graph has been
|
|
17
|
+
// consulted, search is allowed as a fallback and the nudge rides along
|
|
18
|
+
// (we gate the orientation, not every search forever). State is a per-session
|
|
19
|
+
// marker under ~/.neat/hooks/gate/, keyed by session_id, since each hook call
|
|
20
|
+
// is a fresh process.
|
|
21
|
+
//
|
|
22
|
+
// Gate is enabled by `neat hooks --apply --gate` (which persists a flag at
|
|
23
|
+
// ~/.neat/hooks/gate-enabled) and/or the env toggle NEAT_SEARCH_GATE=1;
|
|
24
|
+
// NEAT_SEARCH_GATE=0 forces nudge-only even when the flag is set.
|
|
13
25
|
//
|
|
14
26
|
// Wired by `neat hooks --apply`, which copies this script under ~/.neat/hooks/
|
|
15
27
|
// and adds a PreToolUse entry to ~/.claude/settings.json. The hook itself is
|
|
@@ -21,16 +33,63 @@
|
|
|
21
33
|
// two byte-aligned.
|
|
22
34
|
|
|
23
35
|
import process from 'node:process'
|
|
36
|
+
import fs from 'node:fs'
|
|
37
|
+
import path from 'node:path'
|
|
38
|
+
import os from 'node:os'
|
|
24
39
|
|
|
25
40
|
// grep-family and file-finder binaries. Matched at a word boundary so a path
|
|
26
41
|
// like agent.ts does not trip it, and after a shell separator so the binary in
|
|
27
42
|
// a pipe (... | grep foo) still counts.
|
|
28
43
|
const SEARCH_BINARY = /(?:^|[\s|;&(){}])(?:grep|egrep|fgrep|rg|ripgrep|ag|ack|find|fd)(?=\s|$)/
|
|
44
|
+
// `neat ask …` in a Bash command — the CLI form of consulting the graph.
|
|
45
|
+
const ASK_BASH = /(?:^|[\s|;&(){}])neat\s+ask\b/
|
|
29
46
|
|
|
30
47
|
function bashLooksLikeSearch(command) {
|
|
31
48
|
return typeof command === 'string' && SEARCH_BINARY.test(command)
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
// ~/.neat, overridable via NEAT_HOME (matches the CLI + tests).
|
|
52
|
+
function neatHome() {
|
|
53
|
+
const override = process.env.NEAT_HOME
|
|
54
|
+
return override && override.length > 0 ? path.resolve(override) : path.join(os.homedir(), '.neat')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Gate on when NEAT_SEARCH_GATE=1, or when `neat hooks --apply --gate` persisted
|
|
58
|
+
// the flag. NEAT_SEARCH_GATE=0 forces it off regardless. Default: nudge only.
|
|
59
|
+
function gateEnabled() {
|
|
60
|
+
const env = process.env.NEAT_SEARCH_GATE
|
|
61
|
+
if (env === '0') return false
|
|
62
|
+
if (env === '1') return true
|
|
63
|
+
try {
|
|
64
|
+
return fs.existsSync(path.join(neatHome(), 'hooks', 'gate-enabled'))
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Per-session "ask has run" marker. The first `ask` writes it; every search
|
|
71
|
+
// reads it. session_id scopes the gate to one Claude Code session.
|
|
72
|
+
function markerFor(sessionId) {
|
|
73
|
+
const safe = (sessionId || 'nosession').replace(/[^\w.-]/g, '_')
|
|
74
|
+
return path.join(neatHome(), 'hooks', 'gate', `ask-ran-${safe}`)
|
|
75
|
+
}
|
|
76
|
+
function askHasRun(sessionId) {
|
|
77
|
+
try {
|
|
78
|
+
return fs.existsSync(markerFor(sessionId))
|
|
79
|
+
} catch {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function recordAskRan(sessionId) {
|
|
84
|
+
try {
|
|
85
|
+
const p = markerFor(sessionId)
|
|
86
|
+
fs.mkdirSync(path.dirname(p), { recursive: true })
|
|
87
|
+
fs.writeFileSync(p, new Date().toISOString())
|
|
88
|
+
} catch {
|
|
89
|
+
// best-effort — a missing marker just means the gate stays closed
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
34
93
|
function nudge(toolName) {
|
|
35
94
|
return [
|
|
36
95
|
'NEAT is wired into this project — its live graph already knows this codebase',
|
|
@@ -38,6 +97,7 @@ function nudge(toolName) {
|
|
|
38
97
|
'asking the graph over MCP; it answers with structured, provenance-tagged results',
|
|
39
98
|
'rather than line matches:',
|
|
40
99
|
'',
|
|
100
|
+
' - ask — one plain-language question, routed to the right graph query',
|
|
41
101
|
' - semantic_search — find code/nodes by a natural-language description',
|
|
42
102
|
' - get_dependencies / get_observed_dependencies — what a thing calls, as declared',
|
|
43
103
|
' in code (EXTRACTED) vs. what it actually calls in production (OBSERVED)',
|
|
@@ -49,6 +109,17 @@ function nudge(toolName) {
|
|
|
49
109
|
].join('\n')
|
|
50
110
|
}
|
|
51
111
|
|
|
112
|
+
function denyReason(toolName) {
|
|
113
|
+
return (
|
|
114
|
+
'NEAT gate: consult the graph before text search. Call `neat ask "<your question>"` ' +
|
|
115
|
+
'(or the `ask` MCP tool) first — it resolves the entities in your question and answers ' +
|
|
116
|
+
'with structured, provenance-tagged facts (EXTRACTED from code, OBSERVED from runtime), ' +
|
|
117
|
+
'faster and with production truth that ' + (toolName || 'grep') + ' cannot see. After you have ' +
|
|
118
|
+
'asked the graph once this session, text search is available as a fallback for what the graph ' +
|
|
119
|
+
'does not model (comments, string literals, config minutiae).'
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
52
123
|
function readStdin() {
|
|
53
124
|
return new Promise((resolve) => {
|
|
54
125
|
let data = ''
|
|
@@ -67,19 +138,50 @@ let payload
|
|
|
67
138
|
try {
|
|
68
139
|
payload = JSON.parse(raw)
|
|
69
140
|
} catch {
|
|
70
|
-
// No parseable payload — nothing to
|
|
141
|
+
// No parseable payload — nothing to act on. Let the call proceed.
|
|
71
142
|
process.exit(0)
|
|
72
143
|
}
|
|
73
144
|
|
|
74
145
|
const toolName = typeof payload?.tool_name === 'string' ? payload.tool_name : ''
|
|
75
146
|
const toolInput = payload?.tool_input ?? {}
|
|
147
|
+
const sessionId = typeof payload?.session_id === 'string' ? payload.session_id : ''
|
|
76
148
|
|
|
149
|
+
// 1. Did the agent consult the graph? Any `ask` — the MCP tool or the CLI verb —
|
|
150
|
+
// opens the gate for the rest of this session.
|
|
151
|
+
const isAsk =
|
|
152
|
+
toolName === 'mcp__neat__ask' ||
|
|
153
|
+
toolName === 'ask' ||
|
|
154
|
+
(toolName === 'Bash' && typeof toolInput.command === 'string' && ASK_BASH.test(toolInput.command))
|
|
155
|
+
if (isAsk) {
|
|
156
|
+
recordAskRan(sessionId)
|
|
157
|
+
process.exit(0)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 2. Is this a raw text search? Anything else is a silent no-op.
|
|
77
161
|
let isSearch = false
|
|
78
162
|
if (toolName === 'Grep' || toolName === 'Glob') isSearch = true
|
|
79
163
|
else if (toolName === 'Bash') isSearch = bashLooksLikeSearch(toolInput.command)
|
|
80
164
|
|
|
81
165
|
if (!isSearch) process.exit(0)
|
|
82
166
|
|
|
167
|
+
// 3. GATE: a search before the graph has been consulted → deny and tell the
|
|
168
|
+
// agent what to do instead. Proven to fire even under
|
|
169
|
+
// --dangerously-skip-permissions.
|
|
170
|
+
if (gateEnabled() && !askHasRun(sessionId)) {
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
hookSpecificOutput: {
|
|
174
|
+
hookEventName: 'PreToolUse',
|
|
175
|
+
permissionDecision: 'deny',
|
|
176
|
+
permissionDecisionReason: denyReason(toolName),
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
180
|
+
process.exit(0)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 4. NUDGE: default mode, or gate mode after `ask` has run — allow the search
|
|
184
|
+
// and inject the graph-first note. Never denies.
|
|
83
185
|
process.stdout.write(
|
|
84
186
|
JSON.stringify({
|
|
85
187
|
hookSpecificOutput: {
|
package/package.json
CHANGED