whyfence 0.1.0__tar.gz
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.
- whyfence-0.1.0/LICENSE +21 -0
- whyfence-0.1.0/PKG-INFO +326 -0
- whyfence-0.1.0/README.md +293 -0
- whyfence-0.1.0/pyproject.toml +64 -0
- whyfence-0.1.0/pyproject.toml.orig +54 -0
- whyfence-0.1.0/src/whyfence/__init__.py +5 -0
- whyfence-0.1.0/src/whyfence/__main__.py +3 -0
- whyfence-0.1.0/src/whyfence/cli.py +190 -0
- whyfence-0.1.0/src/whyfence/console.py +21 -0
- whyfence-0.1.0/src/whyfence/ingest/__init__.py +1 -0
- whyfence-0.1.0/src/whyfence/ingest/events.py +573 -0
- whyfence-0.1.0/src/whyfence/ingest/git.py +150 -0
- whyfence-0.1.0/src/whyfence/ingest/symbols.py +446 -0
- whyfence-0.1.0/src/whyfence/linter.py +1244 -0
- whyfence-0.1.0/src/whyfence/mcp_server.py +65 -0
- whyfence-0.1.0/src/whyfence/queries.py +92 -0
- whyfence-0.1.0/src/whyfence/store.py +612 -0
whyfence-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Md Nadeem Akhtar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
whyfence-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whyfence
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Temporal memory for AI coding agents: why your code is the way it is, served over MCP.
|
|
5
|
+
Keywords: ai,coding-agents,mcp,model-context-protocol,claude,claude-code,llm,memory,git,code-history,tree-sitter,developer-tools,linter
|
|
6
|
+
Author: Md Nadeem Akhtar
|
|
7
|
+
Author-email: Md Nadeem Akhtar <nadeemnns2000@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
|
+
Requires-Dist: mcp>=2.3.0,<3
|
|
22
|
+
Requires-Dist: rich>=15.0.0
|
|
23
|
+
Requires-Dist: tree-sitter>=0.26.0
|
|
24
|
+
Requires-Dist: tree-sitter-javascript>=0.25.0
|
|
25
|
+
Requires-Dist: tree-sitter-python>=0.25.0
|
|
26
|
+
Requires-Dist: tree-sitter-typescript>=0.23.2
|
|
27
|
+
Requires-Python: >=3.12
|
|
28
|
+
Project-URL: Homepage, https://github.com/NadeemAkhtar1947/whyfence
|
|
29
|
+
Project-URL: Repository, https://github.com/NadeemAkhtar1947/whyfence
|
|
30
|
+
Project-URL: Issues, https://github.com/NadeemAkhtar1947/whyfence/issues
|
|
31
|
+
Project-URL: Documentation, https://github.com/NadeemAkhtar1947/whyfence/blob/main/README_MCP.md
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# whyfence
|
|
35
|
+
|
|
36
|
+
[](https://github.com/NadeemAkhtar1947/whyfence/actions/workflows/ci.yml)
|
|
37
|
+
[](https://github.com/NadeemAkhtar1947/whyfence/blob/main/LICENSE)
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
**Your coding agent knows *what* your code does. whyfence tells it *why*.**
|
|
41
|
+
|
|
42
|
+
whyfence gives AI coding agents a memory of how your code got the way it is: the
|
|
43
|
+
decisions behind it, the approaches that already failed, the corrections you made,
|
|
44
|
+
and the rules you set. Your agent can check that memory over MCP before it
|
|
45
|
+
"simplifies" something that was deliberately written that way.
|
|
46
|
+
|
|
47
|
+
Local-first. No server, no account, no code leaves your machine.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Chesterton's Fence, for AI agents
|
|
52
|
+
|
|
53
|
+
> There is a fence across a road. A reformer says, "I don't see the use of this; let
|
|
54
|
+
> us clear it away." The wiser one replies: "If you don't see the use of it, I
|
|
55
|
+
> certainly won't let you clear it away. Go away and think. When you can come back
|
|
56
|
+
> and tell me that you *do* see the use of it, I may allow you to destroy it."
|
|
57
|
+
> (paraphrasing G. K. Chesterton)
|
|
58
|
+
|
|
59
|
+
Coding agents meet fences all day: a retry loop that looks redundant, a lock that
|
|
60
|
+
looks unnecessary, a model called `TaskFile` instead of the obvious `File`. They
|
|
61
|
+
can read the code, but they can't see why it is there. That knowledge lives in old
|
|
62
|
+
commit messages, in your head, and in last week's agent session, which the new
|
|
63
|
+
session has forgotten.
|
|
64
|
+
|
|
65
|
+
So agents tear fences down. They re-add the retry you removed on purpose, retry the
|
|
66
|
+
approach that deadlocked on Tuesday, or rename `TaskFile` back to `File` and break
|
|
67
|
+
every import of `fastapi.File`.
|
|
68
|
+
|
|
69
|
+
whyfence collects that "why" from places you already have it and pins it to the
|
|
70
|
+
function it is about:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
why("app.models.TaskFile")
|
|
74
|
+
|
|
75
|
+
decision · 2026-10-31 · from a Claude Code session
|
|
76
|
+
"The file model is `TaskFile`, not `File`, because `File` would clash with
|
|
77
|
+
`fastapi.File`, which the documents and invoice routers already import."
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## What it remembers
|
|
83
|
+
|
|
84
|
+
| Kind | Where it comes from | Example |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `decision` | Commit messages (`Why:`, "because", "so that"); the agent's own stated choices in sessions | "`Decimal` instead of `float` to avoid rounding errors" |
|
|
87
|
+
| `constraint` | Rules you state in a session or a commit | "Never call `send()` from the event loop" |
|
|
88
|
+
| `correction` | You pushing back on the agent | "No, don't retry inside `refresh()`; the HTTP client already retries" |
|
|
89
|
+
| `dead_end` | The agent realizing an approach was wrong | "Turns out `celery.current_app` is a lazy proxy, so patching it doesn't stick" |
|
|
90
|
+
| `incident` | `fix:` commits, "Fixes #12", bugs, crashes | "fix: refresh tokens before they expire" |
|
|
91
|
+
| `revert` | `Revert "…"`, "This reverts commit …" | |
|
|
92
|
+
|
|
93
|
+
Each event is linked to the **functions, methods and classes** it is about, and
|
|
94
|
+
those links follow the code through history. whyfence indexes every Python,
|
|
95
|
+
JavaScript and TypeScript symbol at every commit and keeps its identity when it
|
|
96
|
+
is renamed, moved to another file, or renamed and edited in the same commit.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Try it in one minute
|
|
101
|
+
|
|
102
|
+
```console
|
|
103
|
+
$ git clone https://github.com/NadeemAkhtar1947/whyfence && cd whyfence
|
|
104
|
+
$ ./demo.sh
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
[`demo.sh`](https://github.com/NadeemAkhtar1947/whyfence/blob/main/demo.sh) builds a throwaway repository and a mock Claude Code session in
|
|
108
|
+
a temp folder, then runs `whyfence lint`, `whyfence ingest`, `whyfence why` and
|
|
109
|
+
`whyfence history` on them. It needs only bash, git and
|
|
110
|
+
[uv](https://docs.astral.sh/uv/), and it never touches your own repositories or
|
|
111
|
+
`~/.claude`. The `why` step prints what an agent learns before touching `refresh()`:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
app.tokens.refresh function · python
|
|
115
|
+
constraint 2026-02-10 From now on, never call `refresh` from the request
|
|
116
|
+
path; the background renewer owns it.
|
|
117
|
+
transcript · claude-code · inferred
|
|
118
|
+
dead_end 2026-02-10 I see the issue now: retrying here stacked on top of
|
|
119
|
+
the client's retries. I'll revert the loop.
|
|
120
|
+
transcript · claude-code · inferred
|
|
121
|
+
correction 2026-02-10 No, don't retry inside refresh(). The HTTP client
|
|
122
|
+
already retries, so this doubles every request.
|
|
123
|
+
transcript · claude-code · inferred
|
|
124
|
+
incident 2026-02-03 fix: refresh tokens before they expire
|
|
125
|
+
commit c69facf
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Quick start
|
|
131
|
+
|
|
132
|
+
whyfence is alpha and needs Python 3.12 or newer.
|
|
133
|
+
|
|
134
|
+
```console
|
|
135
|
+
$ pip install whyfence
|
|
136
|
+
$ whyfence ingest /path/to/your/repo
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
On the small repository `demo.sh` builds, that prints:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
Read 3 commits from demo-app: 3 new, 3 indexed in demo-app/.whyfence/index.db
|
|
143
|
+
Walked 3 commits: 2 new symbols, 5 versions, 2 renamed or moved, 0 deleted
|
|
144
|
+
· 2 symbols indexed
|
|
145
|
+
Found 1 event in commit messages and 3 events in 1 Claude Code session,
|
|
146
|
+
with 4 symbol links
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Then connect your agent. For Claude Code, add `.mcp.json` to your repository
|
|
150
|
+
(this uses [uvx](https://docs.astral.sh/uv/); with a plain `pip install`, use
|
|
151
|
+
`"command": "whyfence", "args": ["mcp"]` instead):
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"mcpServers": {
|
|
156
|
+
"whyfence": {
|
|
157
|
+
"type": "stdio",
|
|
158
|
+
"command": "uvx",
|
|
159
|
+
"args": ["whyfence", "mcp"]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Claude Desktop and other clients: see [README_MCP.md](https://github.com/NadeemAkhtar1947/whyfence/blob/main/README_MCP.md).
|
|
166
|
+
|
|
167
|
+
Re-run `whyfence ingest` whenever you like. It only processes new commits and
|
|
168
|
+
sessions, and it rebuilds symbol history automatically if history was rewritten.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Temporal memory over MCP
|
|
173
|
+
|
|
174
|
+
`whyfence mcp` serves two read-only tools:
|
|
175
|
+
|
|
176
|
+
**`why(symbol_name)`**: everything recorded about why a symbol is the way it is,
|
|
177
|
+
newest first, each with its source (commit or session) and how confident the
|
|
178
|
+
extraction is.
|
|
179
|
+
|
|
180
|
+
**`history(symbol_name)`**: the symbol's timeline: each version, the commit that
|
|
181
|
+
introduced it, the commit that replaced it, and its name, file and lines at the time.
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"symbol": {"name": "app.tokens.refresh_token", "kind": "function"},
|
|
186
|
+
"versions": [
|
|
187
|
+
{"name": "app.auth.refresh", "file": "app/auth.py",
|
|
188
|
+
"valid_from": {"sha": "3f1c…", "subject": "add auth"}},
|
|
189
|
+
{"name": "app.session.refresh", "file": "app/session.py",
|
|
190
|
+
"valid_from": {"sha": "9ab2…", "subject": "rename the file"}},
|
|
191
|
+
{"name": "app.tokens.refresh", "file": "app/tokens.py",
|
|
192
|
+
"valid_from": {"sha": "c47e…", "subject": "move refresh to its own module"}},
|
|
193
|
+
{"name": "app.tokens.refresh_token", "file": "app/tokens.py",
|
|
194
|
+
"valid_from": {"sha": "e810…", "subject": "rename and edit at once"},
|
|
195
|
+
"current": true}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Names can be qualified (`app.auth.refresh`), bare (`refresh`) or a symbol id. An
|
|
201
|
+
ambiguous bare name returns the candidates instead of guessing.
|
|
202
|
+
|
|
203
|
+
The same answers are available from the command line, for you or for scripts:
|
|
204
|
+
|
|
205
|
+
```console
|
|
206
|
+
$ whyfence why refresh # readable summary
|
|
207
|
+
$ whyfence history app.auth.refresh
|
|
208
|
+
$ whyfence why refresh --json # exactly what the MCP tool returns
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The server tells agents to call `why` before changing code that looks wrong or
|
|
212
|
+
redundant, and to treat recorded constraints and dead ends as binding unless they
|
|
213
|
+
have new information.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## `whyfence lint`: catch stale agent instructions
|
|
218
|
+
|
|
219
|
+
Agents follow `CLAUDE.md` and `AGENTS.md` literally. When a script is renamed or a
|
|
220
|
+
make target is deleted, every session keeps running a command that no longer
|
|
221
|
+
exists. `whyfence lint` checks every command in those files against what it would
|
|
222
|
+
actually run:
|
|
223
|
+
|
|
224
|
+
```console
|
|
225
|
+
$ whyfence lint examples/stale-repo
|
|
226
|
+
|
|
227
|
+
examples/stale-repo/CLAUDE.md 7 commands · 5 stale
|
|
228
|
+
✗ L3 npm run lint no `lint` script in package.json
|
|
229
|
+
→ did you mean npm run lint:all?
|
|
230
|
+
✗ L7 npm run start no `start` script in web/package.json
|
|
231
|
+
✗ L8 make -C api tests no `tests` target in api/Makefile
|
|
232
|
+
→ did you mean make -C api test?
|
|
233
|
+
✗ L9 make migrate no Makefile in ./
|
|
234
|
+
→ did you mean make -C api migrate?
|
|
235
|
+
✗ L12 make deploy no Makefile in ./
|
|
236
|
+
|
|
237
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
238
|
+
│ ✗ 5 stale ? 0 unverifiable ✓ 2 verified │
|
|
239
|
+
│ 7 commands across 1 file. Agents reading these files will run commands │
|
|
240
|
+
│ that no longer exist. │
|
|
241
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
| Command | Checked against |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `make test`, `make -C api migrate` | Makefile targets, including `include`d files |
|
|
247
|
+
| `npm run dev`, `yarn lint`, `pnpm test`, `bun run build` | `package.json` scripts and package binaries |
|
|
248
|
+
| `uv run serve`, `poetry run serve` | `[project.scripts]`, declared dependencies, `.venv` |
|
|
249
|
+
| `python -m app.jobs`, `uv run -m app` | local modules (with `__main__.py`), stdlib, dependencies |
|
|
250
|
+
| `python scripts/seed.py`, `./scripts/release.sh` | the file on disk |
|
|
251
|
+
|
|
252
|
+
It follows `cd dir &&` and directory flags, suggests the closest match (including
|
|
253
|
+
targets that moved into a subdirectory), and reports anything that depends on
|
|
254
|
+
runtime state as *unverifiable* instead of guessing. It exits with `1` on stale
|
|
255
|
+
commands, so it works as a CI gate:
|
|
256
|
+
|
|
257
|
+
```console
|
|
258
|
+
$ whyfence lint --json # machine-readable
|
|
259
|
+
$ whyfence lint --strict # also fail on unverifiable commands
|
|
260
|
+
$ whyfence lint --exclude docs # skip paths (examples/ is skipped by default)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## How it works
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
git history ──► commits ──► symbol index (tree-sitter, every commit, first-parent)
|
|
269
|
+
│ identity: name → body fingerprint → token similarity
|
|
270
|
+
▼
|
|
271
|
+
commit messages ─┐ symbol_versions ◄── history()
|
|
272
|
+
Claude Code ├─► events ──► event_links ◄── why()
|
|
273
|
+
transcripts ─────┘
|
|
274
|
+
SQLite (WAL, FTS5) in .whyfence/index.db
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- **Symbol identity.** Each definition is fingerprinted with its own name removed
|
|
278
|
+
and whitespace collapsed. A commit's definitions are matched to existing symbols
|
|
279
|
+
by name in the same file, then by fingerprint anywhere in the commit (renames and
|
|
280
|
+
moves), then by token similarity (renamed *and* edited). Only code, file or name
|
|
281
|
+
changes create a new version; line shifts don't.
|
|
282
|
+
- **Events.** Commit events link to the symbols that commit changed. Session events
|
|
283
|
+
link through the edits the agent made around them, located in today's code and
|
|
284
|
+
re-parsed, plus symbols named in the text. Broad events (a feature commit
|
|
285
|
+
touching 80 functions) keep only the symbols they name.
|
|
286
|
+
- **Precision over recall.** Extraction is deterministic pattern matching, tuned on
|
|
287
|
+
real sessions. It skips tool output, subagent chatter, the agent's private
|
|
288
|
+
reasoning and Claude Code's own compaction summaries. A missing event costs
|
|
289
|
+
you nothing; a wrong one teaches your agent something false.
|
|
290
|
+
|
|
291
|
+
## Privacy
|
|
292
|
+
|
|
293
|
+
Everything stays in `.whyfence/index.db` inside your repository, which you can
|
|
294
|
+
gitignore and delete at any time; it is rebuilt from history. Transcripts are read
|
|
295
|
+
from `~/.claude/projects` for sessions in, under or above the repository
|
|
296
|
+
(`--no-transcripts` to skip them). Obvious secrets (API keys, tokens) are redacted
|
|
297
|
+
before anything is stored. whyfence makes no network calls.
|
|
298
|
+
|
|
299
|
+
## Status and roadmap
|
|
300
|
+
|
|
301
|
+
whyfence is an early MVP. It works end to end, but its extraction heuristics have
|
|
302
|
+
only been tuned on a handful of real projects.
|
|
303
|
+
|
|
304
|
+
- [x] Stale-context linter for `CLAUDE.md` / `AGENTS.md`
|
|
305
|
+
- [x] Symbol history with rename and move tracking (Python, JavaScript, TypeScript)
|
|
306
|
+
- [x] Events from commit messages and Claude Code transcripts
|
|
307
|
+
- [x] MCP server: `why`, `history`
|
|
308
|
+
- [ ] Team memory: a shared, append-only event log committed to the repo
|
|
309
|
+
- [ ] More agents' transcripts (Codex, Cursor, Gemini CLI)
|
|
310
|
+
- [ ] More languages (Go, Rust, Java)
|
|
311
|
+
- [ ] A `rules` tool that serves every standing constraint at session start
|
|
312
|
+
|
|
313
|
+
## Development
|
|
314
|
+
|
|
315
|
+
```console
|
|
316
|
+
$ git clone https://github.com/NadeemAkhtar1947/whyfence && cd whyfence
|
|
317
|
+
$ uv sync
|
|
318
|
+
$ uv run pytest
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The test suite includes an end-to-end check that indexes whyfence's own git
|
|
322
|
+
history and verifies every recorded version against Python's `ast`.
|
|
323
|
+
|
|
324
|
+
## License
|
|
325
|
+
|
|
326
|
+
[MIT](https://github.com/NadeemAkhtar1947/whyfence/blob/main/LICENSE)
|
whyfence-0.1.0/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# whyfence
|
|
2
|
+
|
|
3
|
+
[](https://github.com/NadeemAkhtar1947/whyfence/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/NadeemAkhtar1947/whyfence/blob/main/LICENSE)
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**Your coding agent knows *what* your code does. whyfence tells it *why*.**
|
|
8
|
+
|
|
9
|
+
whyfence gives AI coding agents a memory of how your code got the way it is: the
|
|
10
|
+
decisions behind it, the approaches that already failed, the corrections you made,
|
|
11
|
+
and the rules you set. Your agent can check that memory over MCP before it
|
|
12
|
+
"simplifies" something that was deliberately written that way.
|
|
13
|
+
|
|
14
|
+
Local-first. No server, no account, no code leaves your machine.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Chesterton's Fence, for AI agents
|
|
19
|
+
|
|
20
|
+
> There is a fence across a road. A reformer says, "I don't see the use of this; let
|
|
21
|
+
> us clear it away." The wiser one replies: "If you don't see the use of it, I
|
|
22
|
+
> certainly won't let you clear it away. Go away and think. When you can come back
|
|
23
|
+
> and tell me that you *do* see the use of it, I may allow you to destroy it."
|
|
24
|
+
> (paraphrasing G. K. Chesterton)
|
|
25
|
+
|
|
26
|
+
Coding agents meet fences all day: a retry loop that looks redundant, a lock that
|
|
27
|
+
looks unnecessary, a model called `TaskFile` instead of the obvious `File`. They
|
|
28
|
+
can read the code, but they can't see why it is there. That knowledge lives in old
|
|
29
|
+
commit messages, in your head, and in last week's agent session, which the new
|
|
30
|
+
session has forgotten.
|
|
31
|
+
|
|
32
|
+
So agents tear fences down. They re-add the retry you removed on purpose, retry the
|
|
33
|
+
approach that deadlocked on Tuesday, or rename `TaskFile` back to `File` and break
|
|
34
|
+
every import of `fastapi.File`.
|
|
35
|
+
|
|
36
|
+
whyfence collects that "why" from places you already have it and pins it to the
|
|
37
|
+
function it is about:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
why("app.models.TaskFile")
|
|
41
|
+
|
|
42
|
+
decision · 2026-10-31 · from a Claude Code session
|
|
43
|
+
"The file model is `TaskFile`, not `File`, because `File` would clash with
|
|
44
|
+
`fastapi.File`, which the documents and invoice routers already import."
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## What it remembers
|
|
50
|
+
|
|
51
|
+
| Kind | Where it comes from | Example |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `decision` | Commit messages (`Why:`, "because", "so that"); the agent's own stated choices in sessions | "`Decimal` instead of `float` to avoid rounding errors" |
|
|
54
|
+
| `constraint` | Rules you state in a session or a commit | "Never call `send()` from the event loop" |
|
|
55
|
+
| `correction` | You pushing back on the agent | "No, don't retry inside `refresh()`; the HTTP client already retries" |
|
|
56
|
+
| `dead_end` | The agent realizing an approach was wrong | "Turns out `celery.current_app` is a lazy proxy, so patching it doesn't stick" |
|
|
57
|
+
| `incident` | `fix:` commits, "Fixes #12", bugs, crashes | "fix: refresh tokens before they expire" |
|
|
58
|
+
| `revert` | `Revert "…"`, "This reverts commit …" | |
|
|
59
|
+
|
|
60
|
+
Each event is linked to the **functions, methods and classes** it is about, and
|
|
61
|
+
those links follow the code through history. whyfence indexes every Python,
|
|
62
|
+
JavaScript and TypeScript symbol at every commit and keeps its identity when it
|
|
63
|
+
is renamed, moved to another file, or renamed and edited in the same commit.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Try it in one minute
|
|
68
|
+
|
|
69
|
+
```console
|
|
70
|
+
$ git clone https://github.com/NadeemAkhtar1947/whyfence && cd whyfence
|
|
71
|
+
$ ./demo.sh
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
[`demo.sh`](https://github.com/NadeemAkhtar1947/whyfence/blob/main/demo.sh) builds a throwaway repository and a mock Claude Code session in
|
|
75
|
+
a temp folder, then runs `whyfence lint`, `whyfence ingest`, `whyfence why` and
|
|
76
|
+
`whyfence history` on them. It needs only bash, git and
|
|
77
|
+
[uv](https://docs.astral.sh/uv/), and it never touches your own repositories or
|
|
78
|
+
`~/.claude`. The `why` step prints what an agent learns before touching `refresh()`:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
app.tokens.refresh function · python
|
|
82
|
+
constraint 2026-02-10 From now on, never call `refresh` from the request
|
|
83
|
+
path; the background renewer owns it.
|
|
84
|
+
transcript · claude-code · inferred
|
|
85
|
+
dead_end 2026-02-10 I see the issue now: retrying here stacked on top of
|
|
86
|
+
the client's retries. I'll revert the loop.
|
|
87
|
+
transcript · claude-code · inferred
|
|
88
|
+
correction 2026-02-10 No, don't retry inside refresh(). The HTTP client
|
|
89
|
+
already retries, so this doubles every request.
|
|
90
|
+
transcript · claude-code · inferred
|
|
91
|
+
incident 2026-02-03 fix: refresh tokens before they expire
|
|
92
|
+
commit c69facf
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Quick start
|
|
98
|
+
|
|
99
|
+
whyfence is alpha and needs Python 3.12 or newer.
|
|
100
|
+
|
|
101
|
+
```console
|
|
102
|
+
$ pip install whyfence
|
|
103
|
+
$ whyfence ingest /path/to/your/repo
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
On the small repository `demo.sh` builds, that prints:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
Read 3 commits from demo-app: 3 new, 3 indexed in demo-app/.whyfence/index.db
|
|
110
|
+
Walked 3 commits: 2 new symbols, 5 versions, 2 renamed or moved, 0 deleted
|
|
111
|
+
· 2 symbols indexed
|
|
112
|
+
Found 1 event in commit messages and 3 events in 1 Claude Code session,
|
|
113
|
+
with 4 symbol links
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then connect your agent. For Claude Code, add `.mcp.json` to your repository
|
|
117
|
+
(this uses [uvx](https://docs.astral.sh/uv/); with a plain `pip install`, use
|
|
118
|
+
`"command": "whyfence", "args": ["mcp"]` instead):
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"whyfence": {
|
|
124
|
+
"type": "stdio",
|
|
125
|
+
"command": "uvx",
|
|
126
|
+
"args": ["whyfence", "mcp"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Claude Desktop and other clients: see [README_MCP.md](https://github.com/NadeemAkhtar1947/whyfence/blob/main/README_MCP.md).
|
|
133
|
+
|
|
134
|
+
Re-run `whyfence ingest` whenever you like. It only processes new commits and
|
|
135
|
+
sessions, and it rebuilds symbol history automatically if history was rewritten.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Temporal memory over MCP
|
|
140
|
+
|
|
141
|
+
`whyfence mcp` serves two read-only tools:
|
|
142
|
+
|
|
143
|
+
**`why(symbol_name)`**: everything recorded about why a symbol is the way it is,
|
|
144
|
+
newest first, each with its source (commit or session) and how confident the
|
|
145
|
+
extraction is.
|
|
146
|
+
|
|
147
|
+
**`history(symbol_name)`**: the symbol's timeline: each version, the commit that
|
|
148
|
+
introduced it, the commit that replaced it, and its name, file and lines at the time.
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"symbol": {"name": "app.tokens.refresh_token", "kind": "function"},
|
|
153
|
+
"versions": [
|
|
154
|
+
{"name": "app.auth.refresh", "file": "app/auth.py",
|
|
155
|
+
"valid_from": {"sha": "3f1c…", "subject": "add auth"}},
|
|
156
|
+
{"name": "app.session.refresh", "file": "app/session.py",
|
|
157
|
+
"valid_from": {"sha": "9ab2…", "subject": "rename the file"}},
|
|
158
|
+
{"name": "app.tokens.refresh", "file": "app/tokens.py",
|
|
159
|
+
"valid_from": {"sha": "c47e…", "subject": "move refresh to its own module"}},
|
|
160
|
+
{"name": "app.tokens.refresh_token", "file": "app/tokens.py",
|
|
161
|
+
"valid_from": {"sha": "e810…", "subject": "rename and edit at once"},
|
|
162
|
+
"current": true}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Names can be qualified (`app.auth.refresh`), bare (`refresh`) or a symbol id. An
|
|
168
|
+
ambiguous bare name returns the candidates instead of guessing.
|
|
169
|
+
|
|
170
|
+
The same answers are available from the command line, for you or for scripts:
|
|
171
|
+
|
|
172
|
+
```console
|
|
173
|
+
$ whyfence why refresh # readable summary
|
|
174
|
+
$ whyfence history app.auth.refresh
|
|
175
|
+
$ whyfence why refresh --json # exactly what the MCP tool returns
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The server tells agents to call `why` before changing code that looks wrong or
|
|
179
|
+
redundant, and to treat recorded constraints and dead ends as binding unless they
|
|
180
|
+
have new information.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## `whyfence lint`: catch stale agent instructions
|
|
185
|
+
|
|
186
|
+
Agents follow `CLAUDE.md` and `AGENTS.md` literally. When a script is renamed or a
|
|
187
|
+
make target is deleted, every session keeps running a command that no longer
|
|
188
|
+
exists. `whyfence lint` checks every command in those files against what it would
|
|
189
|
+
actually run:
|
|
190
|
+
|
|
191
|
+
```console
|
|
192
|
+
$ whyfence lint examples/stale-repo
|
|
193
|
+
|
|
194
|
+
examples/stale-repo/CLAUDE.md 7 commands · 5 stale
|
|
195
|
+
✗ L3 npm run lint no `lint` script in package.json
|
|
196
|
+
→ did you mean npm run lint:all?
|
|
197
|
+
✗ L7 npm run start no `start` script in web/package.json
|
|
198
|
+
✗ L8 make -C api tests no `tests` target in api/Makefile
|
|
199
|
+
→ did you mean make -C api test?
|
|
200
|
+
✗ L9 make migrate no Makefile in ./
|
|
201
|
+
→ did you mean make -C api migrate?
|
|
202
|
+
✗ L12 make deploy no Makefile in ./
|
|
203
|
+
|
|
204
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
205
|
+
│ ✗ 5 stale ? 0 unverifiable ✓ 2 verified │
|
|
206
|
+
│ 7 commands across 1 file. Agents reading these files will run commands │
|
|
207
|
+
│ that no longer exist. │
|
|
208
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
| Command | Checked against |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `make test`, `make -C api migrate` | Makefile targets, including `include`d files |
|
|
214
|
+
| `npm run dev`, `yarn lint`, `pnpm test`, `bun run build` | `package.json` scripts and package binaries |
|
|
215
|
+
| `uv run serve`, `poetry run serve` | `[project.scripts]`, declared dependencies, `.venv` |
|
|
216
|
+
| `python -m app.jobs`, `uv run -m app` | local modules (with `__main__.py`), stdlib, dependencies |
|
|
217
|
+
| `python scripts/seed.py`, `./scripts/release.sh` | the file on disk |
|
|
218
|
+
|
|
219
|
+
It follows `cd dir &&` and directory flags, suggests the closest match (including
|
|
220
|
+
targets that moved into a subdirectory), and reports anything that depends on
|
|
221
|
+
runtime state as *unverifiable* instead of guessing. It exits with `1` on stale
|
|
222
|
+
commands, so it works as a CI gate:
|
|
223
|
+
|
|
224
|
+
```console
|
|
225
|
+
$ whyfence lint --json # machine-readable
|
|
226
|
+
$ whyfence lint --strict # also fail on unverifiable commands
|
|
227
|
+
$ whyfence lint --exclude docs # skip paths (examples/ is skipped by default)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## How it works
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
git history ──► commits ──► symbol index (tree-sitter, every commit, first-parent)
|
|
236
|
+
│ identity: name → body fingerprint → token similarity
|
|
237
|
+
▼
|
|
238
|
+
commit messages ─┐ symbol_versions ◄── history()
|
|
239
|
+
Claude Code ├─► events ──► event_links ◄── why()
|
|
240
|
+
transcripts ─────┘
|
|
241
|
+
SQLite (WAL, FTS5) in .whyfence/index.db
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **Symbol identity.** Each definition is fingerprinted with its own name removed
|
|
245
|
+
and whitespace collapsed. A commit's definitions are matched to existing symbols
|
|
246
|
+
by name in the same file, then by fingerprint anywhere in the commit (renames and
|
|
247
|
+
moves), then by token similarity (renamed *and* edited). Only code, file or name
|
|
248
|
+
changes create a new version; line shifts don't.
|
|
249
|
+
- **Events.** Commit events link to the symbols that commit changed. Session events
|
|
250
|
+
link through the edits the agent made around them, located in today's code and
|
|
251
|
+
re-parsed, plus symbols named in the text. Broad events (a feature commit
|
|
252
|
+
touching 80 functions) keep only the symbols they name.
|
|
253
|
+
- **Precision over recall.** Extraction is deterministic pattern matching, tuned on
|
|
254
|
+
real sessions. It skips tool output, subagent chatter, the agent's private
|
|
255
|
+
reasoning and Claude Code's own compaction summaries. A missing event costs
|
|
256
|
+
you nothing; a wrong one teaches your agent something false.
|
|
257
|
+
|
|
258
|
+
## Privacy
|
|
259
|
+
|
|
260
|
+
Everything stays in `.whyfence/index.db` inside your repository, which you can
|
|
261
|
+
gitignore and delete at any time; it is rebuilt from history. Transcripts are read
|
|
262
|
+
from `~/.claude/projects` for sessions in, under or above the repository
|
|
263
|
+
(`--no-transcripts` to skip them). Obvious secrets (API keys, tokens) are redacted
|
|
264
|
+
before anything is stored. whyfence makes no network calls.
|
|
265
|
+
|
|
266
|
+
## Status and roadmap
|
|
267
|
+
|
|
268
|
+
whyfence is an early MVP. It works end to end, but its extraction heuristics have
|
|
269
|
+
only been tuned on a handful of real projects.
|
|
270
|
+
|
|
271
|
+
- [x] Stale-context linter for `CLAUDE.md` / `AGENTS.md`
|
|
272
|
+
- [x] Symbol history with rename and move tracking (Python, JavaScript, TypeScript)
|
|
273
|
+
- [x] Events from commit messages and Claude Code transcripts
|
|
274
|
+
- [x] MCP server: `why`, `history`
|
|
275
|
+
- [ ] Team memory: a shared, append-only event log committed to the repo
|
|
276
|
+
- [ ] More agents' transcripts (Codex, Cursor, Gemini CLI)
|
|
277
|
+
- [ ] More languages (Go, Rust, Java)
|
|
278
|
+
- [ ] A `rules` tool that serves every standing constraint at session start
|
|
279
|
+
|
|
280
|
+
## Development
|
|
281
|
+
|
|
282
|
+
```console
|
|
283
|
+
$ git clone https://github.com/NadeemAkhtar1947/whyfence && cd whyfence
|
|
284
|
+
$ uv sync
|
|
285
|
+
$ uv run pytest
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The test suite includes an end-to-end check that indexes whyfence's own git
|
|
289
|
+
history and verifies every recorded version against Python's `ast`.
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
[MIT](https://github.com/NadeemAkhtar1947/whyfence/blob/main/LICENSE)
|