@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 CHANGED
@@ -1,35 +1,49 @@
1
- <!-- NEAT graph-first guidance. Paste this block into your agent's project
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 reaches for NEAT's graph before scanning files by hand.
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
- ## Reach for NEAT's graph before grepping
6
+ ## Query the graph FIRST
7
7
 
8
- This project has NEAT wired in. NEAT keeps a live semantic graph of the system —
9
- code, infrastructure, and runtime behaviour fused into one model — reachable over
10
- MCP (and a REST API). Every edge carries provenance: `EXTRACTED` from source,
11
- `OBSERVED` from OpenTelemetry, `INFERRED` where the trace stitcher bridges a gap,
12
- `STALE` when runtime goes quiet so you know how much to trust each answer.
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
- When you need to understand this codebase, query the graph first. It is usually
15
- faster and more accurate than `grep`/`glob`/`find`, and it can tell you what the
16
- system *actually does at runtime*, not only what the source declares.
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
- Reach for these MCP tools before raw text search:
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 (declared, `EXTRACTED`).
22
- - `get_observed_dependencies` — only what a node calls *in production* (`OBSERVED` via OTel).
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 of a node: what breaks if it changes or fails.
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
- Text search is still a fine fallback. If the graph does not have what you need —
31
- comments, string literals, config minutiae, a file NEAT does not model go ahead
32
- and `grep`. The point is order: ask the graph first, then fall back to scanning.
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 (`neatd status`)
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-nudge — a Claude Code PreToolUse hook.
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
- // injects a short note suggesting NEAT's graph tools first. NEAT already holds
7
- // this project's structure, dependencies, and runtime behaviour, so a graph
8
- // query is usually faster and more accurate than scanning files by hand.
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
- // It is a NUDGE, not a gate. The hook only ever adds context; it never denies
11
- // the call and always exits 0, so the search runs regardless. Anything that is
12
- // not a text search is a silent no-op.
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 nudge about. Let the call proceed.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neat.is/claude-skill",
3
- "version": "0.9.2-dev.20260821",
3
+ "version": "0.9.2",
4
4
  "description": "Claude Code skill drop-in for NEAT — wires the @neat.is/mcp server into Claude's MCP config",
5
5
  "license": "BUSL-1.1",
6
6
  "homepage": "https://neat.is",