@davesheffer/hunch 0.15.3 β 0.16.0
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/README.md +117 -381
- package/dist/cli/index.js +4 -18
- package/dist/core/checkreport.js +14 -1
- package/dist/core/version.js +19 -0
- package/dist/mcp/server.js +2 -1
- package/dist/store/hunchStore.js +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,17 @@ cd your-repo && hunch init && hunch backfill --since 90d
|
|
|
18
18
|
hunch why src/some/file.ts # β¦or just ask Claude Code: "why is X built this way?"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
<sub>Works with **Claude Code, Cursor, Copilot & Windsurf** from one shared graph
|
|
21
|
+
<sub>Works with **Claude Code, Cursor, Copilot & Windsurf** from one shared graph.</sub>
|
|
22
|
+
|
|
23
|
+
### π **[Read the full documentation β hunch-pi.vercel.app/docs](https://hunch-pi.vercel.app/docs)**
|
|
24
|
+
|
|
25
|
+
The docs site is the complete reference β setup, every CLI command and MCP tool, the
|
|
26
|
+
guards, troubleshooting. This README is the tour. Jump to:
|
|
27
|
+
[Install](https://hunch-pi.vercel.app/docs#install) Β·
|
|
28
|
+
[MCP setup](https://hunch-pi.vercel.app/docs#mcp) Β·
|
|
29
|
+
[Firmness](https://hunch-pi.vercel.app/docs#firmness) Β·
|
|
30
|
+
[CLI reference](https://hunch-pi.vercel.app/docs#cli) Β·
|
|
31
|
+
[Troubleshooting](https://hunch-pi.vercel.app/docs#doctor)
|
|
22
32
|
|
|
23
33
|
## The problem
|
|
24
34
|
|
|
@@ -48,385 +58,123 @@ came before. Local-first, no documentation toil, no SaaS.
|
|
|
48
58
|
- **Learn**: each commit becomes a structured **Decision** (an ADR); a failing test
|
|
49
59
|
becomes a **Bug** with a ranked suspect list; recurring or severe bugs are promoted
|
|
50
60
|
into **Constraints** (do-not-break invariants) and raise a component's *fragility*.
|
|
51
|
-
- **Ground**:
|
|
61
|
+
- **Ground**: any MCP assistant reads it through an **MCP server**, an auto-maintained
|
|
52
62
|
**`CLAUDE.md`**, and **slash commands** β every answer cites `provenance`
|
|
53
63
|
(source + confidence + evidence), so nothing is a blind assertion.
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm install -g @davesheffer/hunch # puts `hunch` on your PATH
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Or run from source (for hacking on Hunch itself):
|
|
65
|
+
β Concepts in depth: [the reasoning graph](https://hunch-pi.vercel.app/docs#graph) Β·
|
|
66
|
+
[provenance](https://hunch-pi.vercel.app/docs#provenance) Β·
|
|
67
|
+
[time-travel](https://hunch-pi.vercel.app/docs#time-travel)
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
npm install
|
|
67
|
-
npm run build # compiles to dist/
|
|
68
|
-
npm link # optional: puts a global `hunch` on your PATH
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Either way you then type `hunch β¦`. From a source checkout without `npm link`, use
|
|
72
|
-
`node dist/cli/index.js β¦` (or `npm run hunch -- β¦` to run via tsx). The rest of this
|
|
73
|
-
README uses `hunch` for brevity.
|
|
74
|
-
|
|
75
|
-
### 2. (Recommended) make a coding-assistant CLI available
|
|
76
|
-
|
|
77
|
-
Hunch's LLM synthesis is billed to **your subscription** through a coding-assistant
|
|
78
|
-
CLI β **never** a pay-per-token API key (the API key is stripped from the child env).
|
|
79
|
-
Hunch auto-detects the first one present, in this order:
|
|
80
|
-
|
|
81
|
-
| CLI | Subscription | Detected by |
|
|
82
|
-
|---|---|---|
|
|
83
|
-
| `claude` (Claude Code) | Claude Pro/Max | `claude --version` |
|
|
84
|
-
| `codex` (OpenAI Codex) | ChatGPT Plus/Pro | `codex --version` |
|
|
85
|
-
| `cursor-agent` (Cursor) | Cursor | `cursor-agent --version` |
|
|
86
|
-
|
|
87
|
-
If none is installed, Hunch still works using a deterministic structural heuristic
|
|
88
|
-
(lower-confidence drafts). `hunch doctor` tells you which mode you're in; force one
|
|
89
|
-
with `HUNCH_SYNTH_PROVIDER=claude-cli|codex-cli|cursor-agent|deterministic`.
|
|
90
|
-
|
|
91
|
-
### 3. Initialize the repo you want a memory for
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
hunch init # scaffold .hunch/, index, install the post-commit hook,
|
|
95
|
-
# write .mcp.json + slash commands + CLAUDE.md, register the merge driver
|
|
96
|
-
hunch backfill --since 90d # cold start: seed decisions from recent git history
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
`init` writes a `.mcp.json` pointing at *this machine's* node + Hunch β so **reload
|
|
100
|
-
Claude Code in the repo** afterward to pick up the `hunch_*` tools. (Each teammate runs
|
|
101
|
-
`hunch init` once to wire up their own clone; the captured `.hunch/` content is shared
|
|
102
|
-
via git.)
|
|
103
|
-
|
|
104
|
-
> **Prefer `hunch init` (project-local `.mcp.json`) over a global `claude mcp add`.**
|
|
105
|
-
> `.mcp.json` is registered by file path, so it's robust. A global `claude mcp add`
|
|
106
|
-
> writes to `~/.claude.json` keyed by the raw working-directory string β and **on
|
|
107
|
-
> Windows** that's a trap: drive letters are case-insensitive (`c:\` and `C:\` are the
|
|
108
|
-
> same folder) but Claude Code compares the key case-sensitively, so it can create two
|
|
109
|
-
> project blocks for one directory and a session that resolves to the *other* casing
|
|
110
|
-
> sees no `hunch_*` tools (registration looked fine, the tools just aren't there). If
|
|
111
|
-
> you hit this, run **`hunch doctor`** β on Windows it detects the split and heals it
|
|
112
|
-
> (merging the MCP servers across both casings, after backing up `~/.claude.json`).
|
|
113
|
-
> `hunch init` runs the same heal automatically at the end.
|
|
114
|
-
|
|
115
|
-
### 4. Use it
|
|
116
|
-
|
|
117
|
-
```bash
|
|
118
|
-
hunch why src/auth/session.ts # the decisions / bugs / invariants behind a file
|
|
119
|
-
hunch doctor # check git, schema version, and synthesis mode
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
β¦and in Claude Code, just ask: *"why is the session module built this way?"*
|
|
123
|
-
|
|
124
|
-
## Two ways to use it
|
|
125
|
-
|
|
126
|
-
**Through Claude Code (the point).** Once the MCP server is registered, ask questions
|
|
127
|
-
normally and Claude consults Hunch, or invoke the slash commands:
|
|
128
|
-
|
|
129
|
-
| Slash command | What it does |
|
|
130
|
-
|---|---|
|
|
131
|
-
| `/hunch-why <file\|symbol>` | the decisions, invariants, and bug history behind it β with citations |
|
|
132
|
-
| `/hunch-fix <bug>` | fix a bug grounded in past root causes, blast radius, and constraints |
|
|
133
|
-
| `/hunch-fragile` | a fragility report (the riskiest code, with evidence) |
|
|
134
|
-
|
|
135
|
-
The MCP tools Claude calls under the hood: `hunch_why`, `hunch_query`,
|
|
136
|
-
`hunch_check_constraints`, `hunch_get_dependents` (blast radius), `hunch_blast_radius`
|
|
137
|
-
(dependent files + near-violations a change could break indirectly), `hunch_bug_lineage`,
|
|
138
|
-
`hunch_context` (surgical minimal slice for a task), `hunch_timeline` (a target's decision
|
|
139
|
-
history over time), `hunch_merge_verdict` (a cited BLOCK/WARN/PASS over a diff β see below),
|
|
140
|
-
and the write-backs `hunch_record_decision`, `hunch_record_correction` (turn a correction into
|
|
141
|
-
an enforced invariant β see below). `hunch_why` and `hunch_context` take an optional `as_of`
|
|
142
|
-
(commit/tag/branch) to **time-travel** the graph to a past state.
|
|
143
|
-
|
|
144
|
-
### Works with any MCP assistant
|
|
145
|
-
|
|
146
|
-
The Hunch MCP server is **client-agnostic** β one `.hunch/` graph powers every
|
|
147
|
-
assistant. `hunch init` scaffolds each tool's MCP config + ambient grounding so
|
|
148
|
-
they all consult the same memory:
|
|
149
|
-
|
|
150
|
-
| Assistant | MCP config | Grounding file |
|
|
151
|
-
|---|---|---|
|
|
152
|
-
| Claude Code | `.mcp.json` | `CLAUDE.md` + `/hunch-*` slash commands |
|
|
153
|
-
| Cursor | `.cursor/mcp.json` | `.cursor/rules/hunch.mdc` (always-applied) |
|
|
154
|
-
| VS Code (Copilot) | `.vscode/mcp.json` | `.github/copilot-instructions.md` |
|
|
155
|
-
| Codex CLI | `.codex/config.toml` | `AGENTS.md` |
|
|
156
|
-
| Windsurf | `.windsurf/mcp_config.json` | `.windsurf/rules/hunch.md` (always-on) |
|
|
157
|
-
| Anything else | β | `AGENTS.md` (cross-tool standard) |
|
|
158
|
-
|
|
159
|
-
Each writer **merges** into existing files (other MCP servers and your own prose are
|
|
160
|
-
preserved) and is idempotent. Opt out with `hunch init --no-providers`.
|
|
161
|
-
|
|
162
|
-
**Through the CLI** β the same graph, from your terminal:
|
|
163
|
-
|
|
164
|
-
| Command | What |
|
|
165
|
-
|---|---|
|
|
166
|
-
| `hunch init` | scaffold `.hunch/`, index, install hook + merge driver, auto-install the advisory pre-commit guard, install the **Claude Code agent hooks**, and wire up **every assistant** (Claude Code, Cursor, VS Code/Copilot, Codex, Windsurf, AGENTS.md). Flags: `--no-enforce`, `--enforce-strict`, `--no-providers`, `--no-agent-hooks`, `--firmness <level>` |
|
|
167
|
-
| `hunch index` | parse repo β symbols / edges / components (deterministic, no LLM) |
|
|
168
|
-
| `hunch backfill --since 90d` | replay git history β seed decisions |
|
|
169
|
-
| `hunch sync [sha]` | turn a commit into a Decision (run automatically by the hook) |
|
|
170
|
-
| `hunch record-bug --test <id> --message <m>` | capture a Bug from a failing test |
|
|
171
|
-
| `hunch record-constraint "<statement>" [--scope <globs>] [--severity advisory\|warning\|blocking] [--type β¦] [--rationale <t>] [--source-decision <id>]` | record an invariant the code must not break (what `hunch check` + the strict agent hook enforce) |
|
|
172
|
-
| `hunch firmness [off\|advisory\|firm\|strict]` | get/set how firmly the agent hook enforces Hunch before edits (no arg prints the current level) |
|
|
173
|
-
| `hunch test [cmdβ¦]` | run the suite (default `npm test`); auto-capture failures as Bugs (suspects + recurrenceβConstraints), mark passing tests' bugs fixed |
|
|
174
|
-
| `hunch why <path\|symbol> [--as-of <ref>]` | decisions / bugs / constraints explaining a target (flags `β STALE`); `--as-of` time-travels to what was believed at a commit/tag/branch |
|
|
175
|
-
| `hunch timeline <path\|symbol>` | the decision history for a target β what was believed, its valid-time window, and what superseded it |
|
|
176
|
-
| `hunch supersede <old> --by <new>` | mark one decision as replaced by another: closes the old one's valid-time window (invalidate, don't delete) |
|
|
177
|
-
| `hunch query "<q>" [--semantic]` | full-text + graph search (`--semantic` blends in local embeddings) |
|
|
178
|
-
| `hunch embed` | generate local embeddings for semantic recall (opt-in; needs `@huggingface/transformers`) |
|
|
179
|
-
| `hunch context <path\|symbol> [--as-of <ref>]` | minimal relevant slice for a task: invariants β decisions β bugs β blast radius (`--as-of` time-travels) |
|
|
180
|
-
| `hunch fragile` | ranked fragility report with evidence |
|
|
181
|
-
| `hunch check [--staged\|--commit <sha>\|--base <ref>] [--strict] [--format text\|markdown] [--blast]` | guardrail: flag changes touching a do-not-break invariant **directly or via blast radius** (a guarded file that depends on what you changed), **and changes that re-introduce something a decision deliberately retired** (the Regression Guard). `--base <ref>` checks a PR's diff (for CI); `--format markdown` emits a PR comment; `--strict` fails only on a direct, high-confidence, non-stale **blocking** invariant; `--blast` prints the dependency fan-out |
|
|
182
|
-
| `hunch ci` | scaffold the **CI Constraint Guard** β a GitHub Action that runs `hunch check` on every PR, comments the affected invariants/decisions, and fails on a blocking one |
|
|
183
|
-
| `hunch stale [--resync]` | drift: records whose files changed after last verification (`--resync` regenerates stale decisions from their commits) |
|
|
184
|
-
| `hunch review [--accept <id>\|--reject <id>]` | curate: triage / promote / drop low-confidence drafts |
|
|
185
|
-
| `hunch migrate` | upgrade `.hunch/` records to the current schema version |
|
|
186
|
-
| `hunch compact [--apply]` | prune low-value drafts to bound growth (dry-run by default) |
|
|
187
|
-
| `hunch doctor` | environment diagnostics (git, auth mode, schema version, counts) |
|
|
188
|
-
| `hunch mcp` | start the MCP server over stdio (Claude Code connects here) |
|
|
189
|
-
|
|
190
|
-
## Grounding the agent automatically (firmness)
|
|
191
|
-
|
|
192
|
-
Telling an assistant "consult Hunch first" in a prompt is advisory β it drifts. `hunch
|
|
193
|
-
init` instead installs two **Claude Code agent hooks** (in `.claude/settings.json`) so the
|
|
194
|
-
grounding is enforced by the harness, not by the model's memory:
|
|
195
|
-
|
|
196
|
-
- **Before every edit** (`PreToolUse` on `Edit`/`Write`/`MultiEdit`) Hunch injects the
|
|
197
|
-
relevant slice for the file being touched β its decisions, invariants, bug history, and
|
|
198
|
-
blast radius β straight into the model's context.
|
|
199
|
-
- **On every prompt** (`UserPromptSubmit`) it reminds the agent to query Hunch.
|
|
200
|
-
|
|
201
|
-
How hard it pushes is one committed knob β set it once, it applies to the whole team:
|
|
69
|
+
## Getting started
|
|
202
70
|
|
|
203
71
|
```bash
|
|
204
|
-
hunch
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
## Causal Merge Verdict: does this change re-open a closed bug?
|
|
253
|
-
|
|
254
|
-
A diff-only reviewer (CodeRabbit, Greptile) sees *what* changed. It can't see that the line
|
|
255
|
-
you're deleting is the fix for an incident, or that the symbol you're re-adding was
|
|
256
|
-
deliberately retired. Hunch can β because it holds the **why**.
|
|
257
|
-
|
|
258
|
-
`hunch_merge_verdict` (MCP tool) and `hunch check` replay a diff against the graph and return
|
|
259
|
-
one verdict β **BLOCK / WARN / PASS** β that *cites the reasoning*, not just the rule:
|
|
72
|
+
npm install -g @davesheffer/hunch # Node β₯ 20; puts `hunch` on your PATH
|
|
73
|
+
cd your-repo
|
|
74
|
+
hunch init # scaffold .hunch/, index, install hooks, wire up assistants
|
|
75
|
+
hunch backfill --since 90d # cold start: seed decisions from recent git history
|
|
76
|
+
hunch why src/auth/session.ts # β¦then ask your assistant: "why is X built this way?"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`hunch init` scaffolds `.hunch/`, indexes the repo, installs the git hooks + merge driver,
|
|
80
|
+
writes `.mcp.json` + slash commands + an auto-maintained `CLAUDE.md`, and wires up **every
|
|
81
|
+
detected assistant** (Claude Code, Cursor, VS Code/Copilot, Windsurf, Codex) to the same
|
|
82
|
+
graph β merging idempotently into existing files. **Reload your assistant in the repo**
|
|
83
|
+
afterward to pick up the `hunch_*` tools. Each teammate runs `hunch init` once; the
|
|
84
|
+
`.hunch/` content is shared via git.
|
|
85
|
+
|
|
86
|
+
> Synthesis is billed to **your coding-assistant subscription** (Claude/Codex/Cursor CLI),
|
|
87
|
+
> **never** a pay-per-token API key β and falls back to a deterministic heuristic if no CLI
|
|
88
|
+
> is present. Details: [Synthesis & billing](https://hunch-pi.vercel.app/docs#synthesis).
|
|
89
|
+
> On Windows, prefer `hunch init` over a global `claude mcp add`; if tools don't appear,
|
|
90
|
+
> `hunch doctor` heals it ([why](https://hunch-pi.vercel.app/docs#windows)).
|
|
91
|
+
|
|
92
|
+
**Full walkthrough β** [Getting started](https://hunch-pi.vercel.app/docs#install) Β·
|
|
93
|
+
[MCP & assistants](https://hunch-pi.vercel.app/docs#mcp) Β·
|
|
94
|
+
[MCP tools](https://hunch-pi.vercel.app/docs#mcp-tools) Β·
|
|
95
|
+
[slash commands](https://hunch-pi.vercel.app/docs#slash) Β·
|
|
96
|
+
[the 22-command CLI](https://hunch-pi.vercel.app/docs#cli)
|
|
97
|
+
|
|
98
|
+
## Enforcement: memory that holds the line
|
|
99
|
+
|
|
100
|
+
Hunch isn't just recall β it's a set of **guards** that stop the AI (and you) from undoing
|
|
101
|
+
intentional design. All ride the same rails: the pre-edit hook, `hunch check`, the
|
|
102
|
+
`hunch_merge_verdict` MCP tool, and the CI Constraint Guard. How hard they push is one
|
|
103
|
+
committed knob β [**firmness**](https://hunch-pi.vercel.app/docs#firmness)
|
|
104
|
+
(`off` β `advisory` β `firm` β `strict`).
|
|
105
|
+
|
|
106
|
+
### Never Twice β corrections become enforced invariants
|
|
107
|
+
|
|
108
|
+
You tell the agent "no, never call the pay-per-token API here," it complies once, and next
|
|
109
|
+
session it does it again β because the feedback was stored as advisory text. Hunch closes
|
|
110
|
+
that loop: a correction is captured as a first-class **Constraint** (`human_confirmed`) via
|
|
111
|
+
`hunch_record_correction`, and from then on the **same hook + CI guard** hold *every*
|
|
112
|
+
assistant to it. β [docs](https://hunch-pi.vercel.app/docs#never-twice)
|
|
113
|
+
|
|
114
|
+
### Causal Merge Verdict β does this change re-open a closed bug?
|
|
115
|
+
|
|
116
|
+
A diff-only reviewer sees *what* changed; it can't see that the line you're deleting is the
|
|
117
|
+
fix for an incident. Hunch can β `hunch_merge_verdict` replays a diff against the graph and
|
|
118
|
+
returns a cited **BLOCK / WARN / PASS**:
|
|
260
119
|
|
|
261
120
|
```text
|
|
262
121
|
VERDICT: β BLOCK β this change breaks a recorded invariant or re-opens a known bug.
|
|
263
122
|
|
|
264
123
|
β pay() must verify the session before charging β con_pay
|
|
265
124
|
π§ why: "Charge must verify the session first" (dec_pay)
|
|
266
|
-
π guards against: Double-charge on unverified session
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
It's **deterministic** (no LLM): for every invariant *directly* in scope it walks
|
|
270
|
-
constraint β `source_decision` β the bug whose root cause spawned it; it flags invariants
|
|
271
|
-
reached transitively (blast radius, advisory) and any deliberately-retired code the diff
|
|
272
|
-
re-introduces. BLOCK fires only on a direct, high-confidence, non-stale **blocking** invariant
|
|
273
|
-
or a blocking-linked regression β near-hits stay advisory, so it's safe as a merge gate. Call
|
|
274
|
-
it before opening a PR (`{}` checks staged changes; pass `base: "origin/main"` for the range);
|
|
275
|
-
the CI Constraint Guard renders the same cited verdict as a PR comment.
|
|
276
|
-
|
|
277
|
-
## Veto: re-introducing a *rejected* approach is blocked
|
|
278
|
-
|
|
279
|
-
The Regression Guard catches re-adding code a decision deliberately **retired**. But the most
|
|
280
|
-
expensive reversal is re-introducing an approach a decision **rejected** β the
|
|
281
|
-
`alternatives_rejected` that *never existed in code*, so a diff-only reviewer (and the regression
|
|
282
|
-
guard) is blind to it. A fresh session, not knowing, re-adds the very dependency you rejected for
|
|
283
|
-
latency last month.
|
|
284
|
-
|
|
285
|
-
**Veto** closes that gap. A decision can carry **tripwires** β machine-checkable signals (a
|
|
286
|
-
forbidden dependency, symbol, or scoped pattern) for an `alternatives_rejected` entry. When a diff
|
|
287
|
-
re-introduces one, Hunch blocks it with the receipt:
|
|
288
|
-
|
|
289
|
-
```text
|
|
290
|
-
β VETO β this reverses dec_49916d02c9 ("Read-only layer over committed .hunch/ JSON").
|
|
291
|
-
You rejected: "extension queries MCP/API server for data" (adds latency, runtime coupling)
|
|
292
|
-
You chose: read directly from committed JSON, no backend dependency.
|
|
293
|
-
evidence: +import axios (vscode-extension/src/extension.ts)
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
It rides the **same rails** as everything else: `CheckReport.vetoes` lights up `hunch check`, the CI
|
|
297
|
-
guard, and `hunch_merge_verdict` together, and the pre-edit hook denies the live edit (Edit / Write /
|
|
298
|
-
MultiEdit) *before* it's staged β so the agent self-corrects with no human in the loop. Enforcement
|
|
299
|
-
is **deterministic** (a set-intersection over a human-vouched record β no model in the block path)
|
|
300
|
-
and **progressive**: an auto-drafted tripwire only *warns*; `hunch veto backfill` drafts them and
|
|
301
|
-
`hunch review --accept` confirms a decision **and** its tripwires, flipping it from advisory to
|
|
302
|
-
blocking in one keypress. Full design + DX: [docs/veto.md](docs/veto.md).
|
|
303
|
-
|
|
304
|
-
## Semantic search (optional)
|
|
305
|
-
|
|
306
|
-
By default `hunch query` and the `hunch_query` MCP tool use fast keyword (FTS) search β
|
|
307
|
-
zero setup, instant, offline. For recall on *paraphrases* (a question that shares no words
|
|
308
|
-
with the record it should find), opt into **local embeddings**:
|
|
309
|
-
|
|
310
|
-
```bash
|
|
311
|
-
npm i -g @huggingface/transformers # one-time; a local model runtime
|
|
312
|
-
hunch embed # embed your records (first run downloads ~90MB)
|
|
313
|
-
hunch query --semantic "auth token expiry" # hybrid keyword + semantic recall
|
|
125
|
+
π guards against: Double-charge on unverified session (bug_β¦)
|
|
314
126
|
```
|
|
315
127
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
> global (`-g`) install above; running from a source checkout needs it in the repo
|
|
319
|
-
> (`npm i @huggingface/transformers` there). If `hunch embed` reports the model "present
|
|
320
|
-
> but failed to load," the scopes don't match. `hunch doctor` shows the active mode.
|
|
128
|
+
Deterministic (no LLM), and safe as a merge gate β it blocks only on a direct,
|
|
129
|
+
high-confidence, non-stale blocking invariant. β [docs](https://hunch-pi.vercel.app/docs#merge-verdict)
|
|
321
130
|
|
|
322
|
-
|
|
323
|
-
rule) and **opt-in** (the base install stays lean). The long-lived MCP server picks them up
|
|
324
|
-
automatically once present. Vectors live in the derived SQLite index and are reconciled by
|
|
325
|
-
content hash on every `hunch index`, so they never drift from the JSON source of truth. `hunch
|
|
326
|
-
doctor` reports coverage; tune the blend with `HUNCH_RRF_W_FTS` / `HUNCH_RRF_W_SEM` / `HUNCH_RRF_K`.
|
|
131
|
+
### Decision Guard (Veto) β re-introducing a *rejected* approach is blocked
|
|
327
132
|
|
|
328
|
-
|
|
133
|
+
The most expensive reversal is re-adding an approach a decision **rejected** (latency, a
|
|
134
|
+
forbidden dependency) β code that never existed, so a diff reviewer is blind to it. A
|
|
135
|
+
decision carries machine-checkable **tripwires**; re-introduce one and Hunch blocks it with
|
|
136
|
+
the receipt of what you rejected and why. β [docs](https://hunch-pi.vercel.app/docs#veto)
|
|
329
137
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
`revokeSession`; removed `login`; new dep: redis; touches con_004"*, with breaking-change
|
|
334
|
-
consequences. With the `claude` CLI present it upgrades to full LLM synthesis; otherwise
|
|
335
|
-
it stays useful offline. Either way every record is **advisory and cheap to discard** β
|
|
336
|
-
`hunch review` lets you promote the good ones to human-confirmed.
|
|
138
|
+
Plus the **Regression Guard** (re-adding deliberately-retired code) and the
|
|
139
|
+
**[CI Constraint Guard](https://hunch-pi.vercel.app/docs#ci)** (`hunch ci` β a PR gate that
|
|
140
|
+
comments the affected `con_`/`dec_` ids and fails on a blocking one).
|
|
337
141
|
|
|
338
142
|
## Working as a team
|
|
339
143
|
|
|
340
|
-
The `.hunch/` JSON is the **source of truth
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
an installed Hunch registers its MCP server by package name (`npx -y @davesheffer/hunch`)
|
|
347
|
-
rather than an absolute path β so a teammate on Windows, macOS, or Linux shares the same
|
|
348
|
-
`.hunch/` and the same committed config without per-machine fixups.
|
|
144
|
+
The `.hunch/` JSON is the **source of truth** β diffable, reviewable in PRs, synced for free
|
|
145
|
+
over `git push` / `pull`. `hunch init` registers a git **merge driver** so concurrent edits
|
|
146
|
+
merge **by record id** (human-confirmed beats auto, then confidence, then recency). The graph
|
|
147
|
+
is **OS-agnostic**: paths are stored in POSIX form and an installed Hunch registers its MCP
|
|
148
|
+
server by package name, so Windows / macOS / Linux teammates share one memory without
|
|
149
|
+
per-machine fixups. β [docs](https://hunch-pi.vercel.app/docs#team)
|
|
349
150
|
|
|
350
151
|
## Continuous learning (CI)
|
|
351
152
|
|
|
352
|
-
The decision half of the loop is automatic (the post-commit hook). Light up the
|
|
353
|
-
|
|
153
|
+
The decision half of the loop is automatic (the post-commit hook). Light up the bug/constraint
|
|
154
|
+
half by wrapping your test run β it captures failures as **Bugs** (recurrences auto-promote
|
|
155
|
+
**Constraints**) and resolves fixed ones, preserving the runner's exit code:
|
|
354
156
|
|
|
355
157
|
```bash
|
|
356
|
-
hunch test
|
|
357
|
-
hunch test -- pytest -q # any runner: pass the command after `--`
|
|
158
|
+
hunch test # runs `npm test`; any runner: hunch test -- pytest -q
|
|
358
159
|
```
|
|
359
160
|
|
|
360
|
-
|
|
361
|
-
(
|
|
362
|
-
do-not-break **Constraint**), and marks a previously-open bug **fixed** once its test passes
|
|
363
|
-
again. It preserves the runner's exit code, so it's a drop-in CI step:
|
|
364
|
-
|
|
365
|
-
```yaml
|
|
366
|
-
# .github/workflows/ci.yml
|
|
367
|
-
- run: npm ci
|
|
368
|
-
- run: npx hunch test # exits non-zero on failure, just like the suite
|
|
369
|
-
- run: | # persist what was learned (optional)
|
|
370
|
-
git add .hunch && git commit -m "chore(hunch): capture test run" || true
|
|
371
|
-
git push || true
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
Repair drift after refactors with **`hunch stale --resync`** (re-synthesizes stale decisions
|
|
375
|
-
from their commits via the LLM).
|
|
376
|
-
|
|
377
|
-
### Block a PR that breaks memory (CI Constraint Guard)
|
|
378
|
-
|
|
379
|
-
Memory that only *advises* gets ignored. `hunch ci` scaffolds a GitHub Action that turns
|
|
380
|
-
Hunch into a **merge gate**: on every pull request it runs `hunch check` over the diff,
|
|
381
|
-
posts a sticky comment citing the affected `con_`/`dec_` ids, and **fails the check** when
|
|
382
|
-
the PR breaks a *direct, high-confidence, non-stale* blocking invariant, re-adds
|
|
383
|
-
deliberately-retired code, or contradicts an in-force decision.
|
|
384
|
-
|
|
385
|
-
```bash
|
|
386
|
-
hunch ci # writes .github/workflows/hunch-guard.yml β commit it
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
It reasons over **the diff plus the constraints committed in the same git history**, so the
|
|
390
|
-
comment says exactly which decision a change violates ("breaks `con_004` β server-side
|
|
391
|
-
revocation, from `dec_017`"). Make *Hunch Guard* a required status check in branch protection
|
|
392
|
-
to enforce on merge. The hardened strict gate only blocks on high-confidence, non-stale
|
|
393
|
-
invariants β stale / low-confidence / blast-radius hits stay advisory in the comment β so
|
|
394
|
-
it's safe to require on a shared repo. Under the hood it's just
|
|
395
|
-
`hunch check --base origin/<target> --strict --format markdown`.
|
|
396
|
-
|
|
397
|
-
## Maintenance
|
|
161
|
+
Drop `npx hunch test` into CI, and `hunch ci` to scaffold the PR merge gate.
|
|
162
|
+
β [docs](https://hunch-pi.vercel.app/docs#ci)
|
|
398
163
|
|
|
399
|
-
|
|
400
|
-
heuristic? what schema version is on disk? how many records?
|
|
401
|
-
- **`hunch migrate`** β after upgrading Hunch, bring old `.hunch/` records up to the
|
|
402
|
-
current schema (old records are migrated in memory on every read, so reads never break;
|
|
403
|
-
`migrate` persists the upgrade and never drops a record it can't migrate).
|
|
404
|
-
- **`hunch compact --apply`** β auto-captured drafts accumulate; compaction prunes the
|
|
405
|
-
low-value ones (rejected / superseded / stale drafts, resolved low-confidence bugs).
|
|
406
|
-
It **never** removes an accepted/human-confirmed decision, an open bug, a constraint, or
|
|
407
|
-
any record another record still references. Run without `--apply` first to preview.
|
|
164
|
+
## Semantic search (optional)
|
|
408
165
|
|
|
409
|
-
|
|
166
|
+
`hunch query` uses fast keyword search out of the box. For recall on paraphrases, opt into
|
|
167
|
+
**local embeddings** (`npm i -g @huggingface/transformers && hunch embed`) β local, free, and
|
|
168
|
+
opt-in. Vectors live in the derived SQLite index and never drift from the JSON source of truth.
|
|
410
169
|
|
|
411
|
-
|
|
412
|
-
.hunch/
|
|
413
|
-
ββ components/ one JSON file per architecture node (curated, PR-reviewable)
|
|
414
|
-
ββ decisions/ one JSON file per Decision (ADR)
|
|
415
|
-
ββ bugs/ one JSON file per Bug
|
|
416
|
-
ββ constraints/ one JSON file per Constraint (invariant)
|
|
417
|
-
ββ symbols/index.json the symbol graph (high-cardinality, single file)
|
|
418
|
-
ββ edges/index.json the dependency graph
|
|
419
|
-
ββ manifest.json on-disk schema version
|
|
420
|
-
ββ hunch.sqlite DERIVED FTS5 + graph index, rebuilt by `hunch index` (gitignored)
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
Low-volume entities are one file per record so they read cleanly in a PR; the
|
|
424
|
-
high-cardinality symbol/edge graphs are single id-sorted arrays to keep git noise down.
|
|
425
|
-
SQLite is a throwaway index rebuilt from the JSON β only the JSON is committed.
|
|
170
|
+
## VS Code
|
|
426
171
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
172
|
+
A companion **[VS Code extension](vscode-extension/)** (on
|
|
173
|
+
[Open VSX](https://open-vsx.org/extension/davesheffer/hunch-vscode) β VS Code / Cursor /
|
|
174
|
+
Windsurf / VSCodium) brings the graph into the editor: a tree of decisions / invariants /
|
|
175
|
+
bugs / bug-lineage / fragility / stale records, CodeLens summaries, hover with bug history,
|
|
176
|
+
invariants in the Problems panel, an interactive component graph, and a status-bar invariant
|
|
177
|
+
counter. It reads the committed `.hunch/` JSON directly; writes delegate to the `hunch` CLI.
|
|
430
178
|
|
|
431
179
|
## Architecture
|
|
432
180
|
|
|
@@ -435,51 +183,39 @@ src/
|
|
|
435
183
|
ββ core/ types (Zod schema), ids, paths, glob, schema migration, atomic file I/O
|
|
436
184
|
ββ store/ JSON source of truth ββ SQLite/FTS5 derived index; merge driver; compaction
|
|
437
185
|
ββ extractors/ tree-sitter parse, git introspection, the indexer
|
|
438
|
-
ββ synthesis/ write path:
|
|
186
|
+
ββ synthesis/ write path: subscription CLI (Claude/Codex/Cursor) or deterministic fallback
|
|
439
187
|
ββ mcp/ MCP stdio server (the hunch_* tools)
|
|
440
188
|
ββ integrations/ post-commit hook, CLAUDE.md writer, .mcp.json + slash commands, merge driver
|
|
441
189
|
ββ cli/ commander entrypoint
|
|
442
190
|
```
|
|
443
191
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
[Open VSX](https://open-vsx.org/extension/davesheffer/hunch-vscode) β works in
|
|
448
|
-
VS Code / Cursor / Windsurf / VSCodium) brings the graph into the editor: a tree of
|
|
449
|
-
decisions / invariants / bugs / **bug-lineage** / fragility / **stale records**, a
|
|
450
|
-
**CodeLens** summary + per-symbol bug/fragility marks, **hover** with bug history,
|
|
451
|
-
invariants surfaced in the **Problems panel**, overview-ruler hotspot marks, an
|
|
452
|
-
interactive **component graph**, fuzzy **search**, and a status-bar invariant counter.
|
|
453
|
-
It reads the committed `.hunch/` JSON directly (no server, no native deps); writes
|
|
454
|
-
delegate to the `hunch` CLI.
|
|
192
|
+
Everything lives under `.hunch/` as git-tracked JSON (the source of truth); SQLite is a
|
|
193
|
+
throwaway derived index. β [storage layout](https://hunch-pi.vercel.app/docs#storage) Β·
|
|
194
|
+
[DESIGN.md](DESIGN.md) for the full conceptual model.
|
|
455
195
|
|
|
456
196
|
## Notable engineering decisions
|
|
457
197
|
|
|
458
198
|
- **Subscription-billed synthesis, never the API.** The write path drives your Claude
|
|
459
199
|
subscription via the `claude` CLI; `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` are
|
|
460
|
-
stripped from the child env to force subscription auth
|
|
461
|
-
|
|
462
|
-
- **Native `tree-sitter` (0.21.1)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
-
|
|
466
|
-
|
|
467
|
-
- **
|
|
468
|
-
|
|
469
|
-
refuse to rewrite a corrupt index rather than flatten it.
|
|
470
|
-
- **OS-agnostic by construction.** Git emits `/`-separated paths on every platform, so the
|
|
471
|
-
graph is the source of truth in POSIX form; any free-form target is canonicalized before
|
|
472
|
-
comparison, so `src\auth\x.ts` and `src/auth/x.ts` resolve to the same records. Generated
|
|
473
|
-
configs that get committed reference Hunch by package name, never a machine-local path.
|
|
200
|
+
stripped from the child env to force subscription auth. A deterministic, no-LLM fallback
|
|
201
|
+
means the loop never hard-requires credentials.
|
|
202
|
+
- **Native `tree-sitter` (0.21.1)**, not web-tree-sitter β the prebuilt WASM grammars have an
|
|
203
|
+
incompatible ABI; the native bindings ship Node-20 prebuilds and a simpler synchronous API.
|
|
204
|
+
- **`better-sqlite3` pinned to `12.9.0`** β 12.10.x ships no Node-20 prebuild.
|
|
205
|
+
- **Atomic, durable writes.** All `.hunch/` writes go through temp-file + rename; an
|
|
206
|
+
interrupted write can't truncate the index, and `put`/`delete` refuse to rewrite a corrupt one.
|
|
207
|
+
- **OS-agnostic by construction.** Paths are canonicalized to POSIX before comparison, and
|
|
208
|
+
committed configs reference Hunch by package name, never a machine-local path.
|
|
474
209
|
|
|
475
210
|
## Develop
|
|
476
211
|
|
|
477
212
|
```bash
|
|
478
|
-
npm run
|
|
479
|
-
npm
|
|
480
|
-
npm
|
|
213
|
+
npm run dev -- <args> # run the CLI from source via tsx (no build step)
|
|
214
|
+
npm run typecheck # strict tsc β the gate
|
|
215
|
+
npm test # node:test suite
|
|
216
|
+
npm run build # compile to dist/ (the published artifact)
|
|
481
217
|
```
|
|
482
218
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
219
|
+
Hunch is pure TypeScript ESM, Node β₯ 20, licensed **Apache-2.0**. See
|
|
220
|
+
[CONTRIBUTING.md](CONTRIBUTING.md), [DESIGN.md](DESIGN.md), and the full
|
|
221
|
+
[developer docs](https://hunch-pi.vercel.app/docs#develop).
|
package/dist/cli/index.js
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
18
|
-
import {
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { relative } from "node:path";
|
|
20
19
|
import { Command } from "commander";
|
|
21
20
|
import { hunchPaths, findRoot, toPosixTarget } from "../core/paths.js";
|
|
22
21
|
import { looksLikeCorrection, CORRECTION_NUDGE } from "../core/correction.js";
|
|
22
|
+
import { HUNCH_VERSION } from "../core/version.js";
|
|
23
23
|
import { HunchStore } from "../store/hunchStore.js";
|
|
24
24
|
import { selectEmbedder } from "../store/embedder.js";
|
|
25
25
|
import { indexRepo } from "../extractors/indexer.js";
|
|
@@ -45,20 +45,6 @@ import { readManifest, writeManifest, SCHEMA_VERSION } from "../core/migrate.js"
|
|
|
45
45
|
import { mergeHunchJson } from "../store/merge.js";
|
|
46
46
|
import { planCompaction } from "../store/compact.js";
|
|
47
47
|
import { resolveInvocation } from "./invocation.js";
|
|
48
|
-
/** The real package version β read from package.json (at the package root, two up
|
|
49
|
-
* from dist/cli/ in the published tarball, and from src/cli/ in dev) so the CLI
|
|
50
|
-
* never drifts from what npm shipped. A hardcoded literal here silently lied
|
|
51
|
-
* (every release reported 0.1.0 regardless of the actual installed version). */
|
|
52
|
-
const HUNCH_VERSION = (() => {
|
|
53
|
-
try {
|
|
54
|
-
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
55
|
-
const v = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
56
|
-
return typeof v === "string" ? v : "0.0.0";
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return "0.0.0";
|
|
60
|
-
}
|
|
61
|
-
})();
|
|
62
48
|
const program = new Command();
|
|
63
49
|
program.name("hunch").description("Hunch β an Engineering Memory OS: a git-native reasoning graph for your codebase.").version(HUNCH_VERSION);
|
|
64
50
|
let openStore = null;
|
|
@@ -571,7 +557,7 @@ program
|
|
|
571
557
|
if (sources.length > 1)
|
|
572
558
|
return fail(`pick one of --staged / --commit / --base (got ${sources.join(", ")})`);
|
|
573
559
|
const markdown = opts.format === "markdown";
|
|
574
|
-
const emptyReport = { fileCount: 0, strict: !!opts.strict, direct: [], near: [], regressions: [], vetoes: [], strictBlockers: 0, regBlocking: 0, vetoBlocking: 0 };
|
|
560
|
+
const emptyReport = { fileCount: 0, strict: !!opts.strict, direct: [], near: [], regressions: [], vetoes: [], redundant: [], strictBlockers: 0, regBlocking: 0, vetoBlocking: 0 };
|
|
575
561
|
const { store, root } = storeFor();
|
|
576
562
|
// Fail loudly on an unresolvable --base (e.g. CI forgot to fetch the base
|
|
577
563
|
// branch) β otherwise the diff is empty and the guard passes vacuously.
|
|
@@ -647,7 +633,7 @@ const vetoCmd = program
|
|
|
647
633
|
}
|
|
648
634
|
// Render ONLY the veto class β zero the other sections so the shared renderer
|
|
649
635
|
// shows just the rejected-alternative reversals (the rest is `hunch check`).
|
|
650
|
-
const vetoOnly = { ...full, direct: [], near: [], regressions: [], strictBlockers: 0, regBlocking: 0 };
|
|
636
|
+
const vetoOnly = { ...full, direct: [], near: [], regressions: [], redundant: [], strictBlockers: 0, regBlocking: 0 };
|
|
651
637
|
console.log(markdown ? renderMarkdown(vetoOnly) : renderText(vetoOnly));
|
|
652
638
|
if (reportFailsStrict(vetoOnly))
|
|
653
639
|
process.exitCode = 1;
|
package/dist/core/checkreport.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* then renders it as text (terminal, unchanged) or markdown (a PR comment posted
|
|
5
5
|
* by the GitHub Action). The exit-code decision lives with the caller. */
|
|
6
6
|
export function reportIsClean(r) {
|
|
7
|
-
return r.direct.length === 0 && r.near.length === 0 && r.regressions.length === 0 && r.vetoes.length === 0;
|
|
7
|
+
return r.direct.length === 0 && r.near.length === 0 && r.regressions.length === 0 && r.vetoes.length === 0 && r.redundant.length === 0;
|
|
8
8
|
}
|
|
9
9
|
/** True when --strict should FAIL the commit/PR. */
|
|
10
10
|
export function reportFailsStrict(r) {
|
|
@@ -76,6 +76,12 @@ export function renderText(r) {
|
|
|
76
76
|
out.push(` ${v.blocking ? "β" : "β "} ${v.decision} rejected this approach${v.blocking ? " (human-confirmed)" : " (advisory)"}\n you rejected: ${clip(v.alternative)}\n you chose: ${clip(v.chosen)}\n evidence: ${v.evidence.slice(0, 4).join(", ")}`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
if (r.redundant.length) {
|
|
80
|
+
out.push(`${r.direct.length || r.near.length || r.regressions.length || r.vetoes.length ? "\n" : ""}Possibly re-implements ${r.redundant.length} symbol(s) that already exist (advisory β review, never blocks):\n`);
|
|
81
|
+
for (const x of r.redundant) {
|
|
82
|
+
out.push(` β² adds ${x.kind} \`${x.name}\` β already defined in ${x.existingFile}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
79
85
|
if (reportFailsStrict(r)) {
|
|
80
86
|
const reasons = [
|
|
81
87
|
r.strictBlockers ? `${r.strictBlockers} high-confidence blocking invariant(s) directly in scope` : "",
|
|
@@ -142,6 +148,13 @@ export function renderMarkdown(r) {
|
|
|
142
148
|
}
|
|
143
149
|
out.push("");
|
|
144
150
|
}
|
|
151
|
+
if (r.redundant.length) {
|
|
152
|
+
out.push(`### β² Possibly re-implements existing code (advisory)`);
|
|
153
|
+
for (const x of r.redundant) {
|
|
154
|
+
out.push(`- \`${x.name}\` (${x.kind}) β already defined in \`${x.existingFile}\``);
|
|
155
|
+
}
|
|
156
|
+
out.push("");
|
|
157
|
+
}
|
|
145
158
|
out.push("---");
|
|
146
159
|
if (reportFailsStrict(r)) {
|
|
147
160
|
const reasons = [
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The real package version, read once from package.json at the package root β
|
|
2
|
+
* two dirs up from dist/<area>/ in the published tarball, and from src/<area>/ in
|
|
3
|
+
* dev. Shared by the CLI (`--version`) and the MCP server (serverInfo) so neither
|
|
4
|
+
* drifts from what npm shipped; a hardcoded literal silently lies (the MCP server
|
|
5
|
+
* reported 0.1.0 for every release until this). Falls back to "0.0.0" if unreadable. */
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
export const HUNCH_VERSION = (() => {
|
|
10
|
+
try {
|
|
11
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
12
|
+
const v = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
13
|
+
return typeof v === "string" ? v : "0.0.0";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "0.0.0";
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
//# sourceMappingURL=version.js.map
|
package/dist/mcp/server.js
CHANGED
|
@@ -17,6 +17,7 @@ import { buildCorrectionConstraint } from "../core/correction.js";
|
|
|
17
17
|
import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff } from "../extractors/git.js";
|
|
18
18
|
import { formatContext } from "../core/format.js";
|
|
19
19
|
import { renderMarkdown, verdict } from "../core/checkreport.js";
|
|
20
|
+
import { HUNCH_VERSION } from "../core/version.js";
|
|
20
21
|
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
21
22
|
const err = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
22
23
|
// Read-side token budgets: every tool result is injected into a Claude Code
|
|
@@ -60,7 +61,7 @@ export function buildServer(root) {
|
|
|
60
61
|
// the optional model isn't installed). The model then loads lazily on the first
|
|
61
62
|
// hunch_query and stays warm β and hybridSearch degrades to FTS until then.
|
|
62
63
|
const embedderReady = selectEmbedder();
|
|
63
|
-
const server = new McpServer({ name: "hunch", version:
|
|
64
|
+
const server = new McpServer({ name: "hunch", version: HUNCH_VERSION });
|
|
64
65
|
// -- hunch_query ----------------------------------------------------------
|
|
65
66
|
server.registerTool("hunch_query", {
|
|
66
67
|
title: "Query Hunch",
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -443,6 +443,10 @@ export class HunchStore {
|
|
|
443
443
|
const staleIds = new Set(staleRecords.filter((s) => s.kind === "constraint").map((s) => s.id));
|
|
444
444
|
const staleDecisionIds = new Set(staleRecords.filter((s) => s.kind === "decision").map((s) => s.id));
|
|
445
445
|
const vetoes = this.vetoHits(an, files, staleDecisionIds);
|
|
446
|
+
const redundant = this.redundantSymbols(an.addedSymbols, files, {
|
|
447
|
+
movedFrom: [...an.filesRenamed.map((r) => r.from), ...an.filesDeleted],
|
|
448
|
+
removedNames: new Set(an.removedSymbols.map((s) => s.name)),
|
|
449
|
+
});
|
|
446
450
|
const directReport = [...direct.values()].map(({ c, files: fs }) => {
|
|
447
451
|
const stale = staleIds.has(c.id);
|
|
448
452
|
const strictBlocks = isStrictBlocker(c, stale);
|
|
@@ -460,11 +464,64 @@ export class HunchStore {
|
|
|
460
464
|
near: [...near.values()].map(({ c, via }) => ({ id: c.id, severity: c.severity ?? "advisory", statement: c.statement, via })),
|
|
461
465
|
regressions: regHits.map((h) => ({ kind: h.kind, name: h.name, decision: h.decision, title: h.title, reason: h.reason, blocking: h.blocking })),
|
|
462
466
|
vetoes: vetoes.map((v) => ({ decision: v.decision, title: v.title, alternative: v.alternative, chosen: v.chosen, tier: v.tier, evidence: v.evidence, blocking: v.blocks })),
|
|
467
|
+
redundant,
|
|
463
468
|
strictBlockers: directReport.filter((d) => d.strictBlocks).length,
|
|
464
469
|
regBlocking: regHits.filter((h) => h.blocking).length,
|
|
465
470
|
vetoBlocking: vetoes.filter((v) => v.blocks).length,
|
|
466
471
|
};
|
|
467
472
|
}
|
|
473
|
+
/** Sprawl/"this already exists" guard (ADVISORY, never blocks). A symbol the diff
|
|
474
|
+
* ADDS whose name already exists in the indexed graph in a file NOT touched by the
|
|
475
|
+
* diff is a likely re-implementation the agent's local context window couldn't see.
|
|
476
|
+
* Read-only. Heuristic, so noise is controlled: top-level function/class/const only,
|
|
477
|
+
* name length β₯ 4, a stopword list of generic names, deduped, and capped. */
|
|
478
|
+
redundantSymbols(added, files, opts = {}) {
|
|
479
|
+
if (!added.length)
|
|
480
|
+
return [];
|
|
481
|
+
const KINDS = new Set(["function", "class", "const"]);
|
|
482
|
+
const STOP = new Set([
|
|
483
|
+
"index", "handler", "handlers", "default", "main", "run", "start", "stop", "setup", "init",
|
|
484
|
+
"constructor", "render", "create", "update", "build", "parse", "load", "save", "next", "data",
|
|
485
|
+
"value", "item", "items", "props", "state", "config", "options", "result", "route", "routes",
|
|
486
|
+
"app", "server", "client", "test", "tests", "mock", "stub", "helper", "helpers", "util", "utils",
|
|
487
|
+
"types", "schema", "constants", "common", "shared", "base", "model", "models", "view", "store",
|
|
488
|
+
]);
|
|
489
|
+
// Match only against top-level VALUE declarations already in the graph. A method
|
|
490
|
+
// (`obj.close()`) or the file node itself sharing a name is not a re-implementation.
|
|
491
|
+
// ("variable" is kept for forward-compat; the current indexer emits arrow-fn consts
|
|
492
|
+
// as "function", so const re-implementations are still matched.)
|
|
493
|
+
const EXISTING_KINDS = new Set(["function", "class", "variable"]);
|
|
494
|
+
// A symbol carried into a moved/deleted file is being relocated, not duplicated. The
|
|
495
|
+
// changed-file list uses --diff-filter=ACMR, so a sub-threshold move (Add new + Delete
|
|
496
|
+
// old) drops the old path β add the move-from / deleted paths back so their lingering
|
|
497
|
+
// graph entries are not mistaken for a separate "existing" implementation.
|
|
498
|
+
const changed = new Set(files);
|
|
499
|
+
for (const p of opts.movedFrom ?? [])
|
|
500
|
+
changed.add(p);
|
|
501
|
+
const removedNames = opts.removedNames ?? new Set();
|
|
502
|
+
// Only compare within the diff's own top-level root(s). A name that also exists in a
|
|
503
|
+
// test fixture or a separate sub-project (test/, vscode-extension/, site/) is not
|
|
504
|
+
// sprawl in the source under change β different roots, different ownership.
|
|
505
|
+
const roots = new Set(files.map((f) => f.split("/")[0]));
|
|
506
|
+
const symbols = this.json.loadAll("symbols");
|
|
507
|
+
const out = [];
|
|
508
|
+
const seen = new Set();
|
|
509
|
+
for (const sc of added) {
|
|
510
|
+
const name = sc.name;
|
|
511
|
+
if (seen.has(name) || name.length < 4 || !KINDS.has(sc.kind) || STOP.has(name.toLowerCase()))
|
|
512
|
+
continue;
|
|
513
|
+
if (removedNames.has(name))
|
|
514
|
+
continue; // the same name was removed in this diff β moved, not duplicated
|
|
515
|
+
const hit = symbols.find((s) => s.name === name && !changed.has(s.file) && EXISTING_KINDS.has(s.kind) && roots.has(s.file.split("/")[0]));
|
|
516
|
+
if (hit) {
|
|
517
|
+
seen.add(name);
|
|
518
|
+
out.push({ name, kind: sc.kind, existingFile: hit.file });
|
|
519
|
+
if (out.length >= 10)
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
468
525
|
/** Time-travel: the decision history for a target β every decision touching it,
|
|
469
526
|
* newest-first, with its valid-time window and supersession links. Answers
|
|
470
527
|
* "what did we believe, and when/why did it change?" (hunch_timeline). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
6
|
"description": "Hunch β an Engineering Memory OS: a persistent, git-native reasoning graph over a codebase, exposed to Claude Code via MCP.",
|