Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent-memory/plans/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
| [p11-personal-project-coverage-sweep.md](p11-personal-project-coverage-sweep.md) | P11 β€” Personal-Project Coverage Sweep (.hook free win + full validation) | Done |
| [p12-code-extensions-classifier-gap.md](p12-code-extensions-classifier-gap.md) | P12 β€” CODE_EXTENSIONS Classifier Gap (critical: real CLI never saw the new languages) | Done |
| [p13-3d-force-graph-lazy-view.md](p13-3d-force-graph-lazy-view.md) | P13 β€” Lazy-Loaded 3D Force Graph Option (Additive, Opt-In) | Done |
| [p14-obsidian-like-graph-control.md](p14-obsidian-like-graph-control.md) | P14 β€” Obsidian-like Graph Control & Automation | In Progress |
| [p14-obsidian-like-graph-control.md](p14-obsidian-like-graph-control.md) | P14 β€” Obsidian-like Graph Control & Automation | Done β€” doc was stale, corrected 2026-07-18 (auto-open/live-reload/settings panel all shipped in e4e4f9c/d79b6fb) |
| [p15-config-value-coupling.md](p15-config-value-coupling.md) | P15 β€” Opt-In Config Value-Coupling Edges (`shares_value`) | Done (2026-07-04) β€” gate passed ~96%, verified on HA (+1.6% edges) |
| [p16-qualified-node-resolution.md](p16-qualified-node-resolution.md) | P16 β€” Qualified Node Resolution for `path`/`explain` (duplicate-name root cause) | Phase 1 Done (2026-07-04); Phase 2 rejected β€” no evidence needed |
| [p17-post-competitor-audit-roadmap.md](p17-post-competitor-audit-roadmap.md) | P17 β€” Post-Competitor-Audit Roadmap (file-watcher auto-sync, PageRank ranking, 0.18.0 cut, `affected --relation` prefix match, content-as-data hold) | Planning (2026-07-18) β€” item 1 (auto-sync) handed to `dev-architect` next |

## Rejected Ideas (evaluated, decided against β€” not just deferred)

Expand Down
149 changes: 149 additions & 0 deletions agent-memory/plans/file-watcher-auto-sync/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# File-Watcher Auto-Sync β€” Design

Source: P17 item 1 (`agent-memory/plans/p17-post-competitor-audit-roadmap.md`), PR #14.

## Orientation (before design β€” required by this doc's own P17 instruction)

Confirmed by reading the real code, not assumed:

- `graphify watch` (`graphify/watch.py:1010`, dispatched from `graphify/__main__.py:3539`) **already
exists and is fully functional** β€” a real `watchdog`-based daemon (`Observer`/`PollingObserver`,
`FileSystemEventHandler`, 3s debounce, `.graphifyignore`-aware, code/doc-change split, calls the same
`_rebuild_code()` `update` uses). It is not a stub or partial implementation.
- It is **opt-in and foreground-blocking**: `watchdog` is an optional extra (`pyproject.toml:62`,
`watch = ["watchdog"]`, not installed by default), and `watch()`'s main loop is `while True: time.sleep(0.5)`
(`watch.py:1080`) β€” a human must run `graphify watch .` in a dedicated terminal and leave it running.
- **Correction (caught mid-task-design, after this doc's first draft): there IS a third existing
mechanism, missed in the first pass of this same orientation** β€” `graphify/hooks.py` (548 lines,
tested in `tests/test_hooks.py`) installs real **git `post-commit`/`post-checkout` hooks**
(`hooks.py:483`, `install(path)`) that trigger a detached, non-blocking background rebuild
(`_detached_launch`, `hooks.py:230`) after every commit. This is not hypothetical β€” it's installed in
graphify's own dev repo right now; the `graphify-out/.last-refresh.log: No such file or directory`
line that fired on every single `git commit`/`git checkout` throughout this whole session's work *was*
this hook running. It's a separate opt-in command (`graphify hook install`), not bundled into
`graphify install`'s main Claude Code hook setup (`_install_claude_hook`, `__main__.py:1985`) β€” a
discoverability gap in its own right, but not this design's problem to fix.
- **What this corrects, not invalidates**: the git hook fires **on commit**, async/detached. It does
nothing for the much larger window every coding session actually lives in β€” uncommitted edits, mid-task,
which is exactly when an agent needs an accurate graph to validate a fix (this session's own Home-
Assistant/Fitness-Tracker rebuilds happened *before* any commit). So the real remaining gap, precisely:
**no mechanism fires between an agent's edit and their next commit.** `graphify watch` covers this for
a human who remembered to start it; nothing covers it automatically for an agent session.
- **There is still no hook that triggers a rebuild after an AI agent's own edit tool calls specifically.**
Grepped the whole codebase for `PostToolUse` β€” zero matches (this part of the original finding holds).
The only installed Claude Code hook is `PreToolUse` (`_SETTINGS_HOOK`/`_READ_SETTINGS_HOOK`,
`__main__.py:470-524`), which nudges the agent to *query* the graph before reading raw files β€” it says
nothing about keeping the graph fresh after a write.
- **`_install_claude_hook`'s own printed message is currently false advertising**: `__main__.py:1980-1981`
prints *"Claude Code will now check the knowledge graph before answering codebase questions and rebuild
it after code changes"* β€” the second half describes a feature that doesn't exist *for the Claude Code
hook path* (the git-hook path is real, but separate, opt-in, and commit-triggered, not what this
sentence is describing in context β€” it's printed right after installing the query-nudge hook). This
design closes that gap for real, rather than just fixing the sentence.
- This session hit the resulting gap directly twice (Home-Assistant, Fitness-Tracker) and needed a manual
`graphify update .` (once even needing a manual `graphify-out/cache` wipe, since the SHA256 cache tracks
file content, not extractor-code version β€” a narrower issue, out of scope here, only affects people
developing graphify's own extractors).

**Conclusion: the gap is not "missing rebuild engine" (there are already two: `watch`'s filesystem
daemon and `hooks.py`'s git-commit hook) β€” it's "neither existing mechanism covers the uncommitted-edit
window an agent session actually lives in."** This reframes the fix from "port a competitor's
file-watcher" to "add the one missing trigger point between the two graphify already has."

## Strategic Design

This is a CLI/tooling feature, not a business-domain model β€” bounded-context/aggregate/domain-event
vocabulary doesn't map cleanly onto it, so this section states that plainly rather than forcing a fit.
The one real strategic decision is **which mechanism** closes the remaining gap β€” evaluated against the
two that already exist, not just each other:

| | **A. Auto-start the existing daemon** | **B. Hook-triggered incremental update** |
|---|---|---|
| Mechanism | A `SessionStart` hook spawns `graphify watch <project> &` (detached) once per project per machine, if not already running | A `PostToolUse` hook (matcher `Edit\|Write\|MultiEdit`) runs `graphify update <project> &` (detached) after every agent write |
| New dependency | Requires `watchdog` installed (optional extra) β€” must degrade gracefully when absent | None β€” reuses the same `update` path CLAUDE.md already documents running manually |
| New failure modes | Daemon lifecycle: orphaned processes across session restarts/crashes, needs a PID file + liveness check, needs cleanup on the *last* session for a project to end (not obvious when that is with multiple concurrent agent sessions) | None new β€” a background `update` call is a fire-and-forget subprocess, same shape as `webbrowser.open`'s pattern already in this codebase (P14) |
| Solves | Both agent edits AND a human editing the same repo in an IDE in parallel | Only edits the agent itself makes through its own tool calls |
| Redundant work | None (event-driven on real filesystem changes) | Could double-fire on `MultiEdit` (N file edits in one call) or fire once per `Edit` in a rapid sequence β€” needs the SAME debounce discipline `watch.py` already has, just reimplemented at the hook layer |

**Decision: B (hook-triggered incremental update), not A.** Reasoning:
1. B closes the *exact* gap this session hit β€” an agent forgetting to rebuild after its own edits β€” with
zero new dependency and zero new process-lifecycle surface to get wrong. A solves a **different**,
real-but-unconfirmed problem (a human editing in parallel) that no evidence from this session or the
competitor audit specifically calls for.
2. B directly fixes `_install_claude_hook`'s existing false claim (a `PreToolUse`+`PostToolUse` pair,
mirroring the two hooks that already exist for reads) rather than introducing a third, heavier
mechanism alongside it.
3. A remains available as-is for the "human in an IDE" case β€” this design does not remove or change
`graphify watch`, it just doesn't make it the default. Revisit A only if a real case surfaces where a
human's parallel edits go unnoticed because no agent tool call touched that file.

## Tactical Design

No entities/aggregates in the DDD sense β€” the "thing being modified" is a JSON file on disk
(`graph.json`), already modeled by the existing `_rebuild_code()` pipeline, not a new domain concept.
The only new "event" is procedural: **AgentWroteFile β†’ debounced background graph update**, described
as a state machine rather than a domain event stream (this is infrastructure, not a bounded context with
its own lifecycle):

```
Edit/Write/MultiEdit tool call completes
-> PostToolUse hook fires, matcher "Edit|Write|MultiEdit"
-> parse tool_input.file_path (or file_path list for MultiEdit) from stdin JSON (same
python3 -c "..." pattern _SETTINGS_HOOK/_READ_SETTINGS_HOOK already use)
-> bail (fail open, exit 0) if: graphify-out/graph.json doesn't exist, OR file_path is
under graphify-out/ (avoid a self-triggered rebuild loop), OR the extension isn't in
a watched set (mirror watch.py's _WATCHED_EXTENSIONS)
-> touch a debounce marker file under graphify-out/.pending-update (mtime = now); if a
background updater for this project is already running (PID file check, same
_rebuild_lock() pattern watch.py:93 already uses for its own concurrency guard),
do nothing further - it will pick up the new marker
-> else spawn `graphify update <project-root> &` detached (setsid/nohup-equivalent,
stdout/stderr to graphify-out/.update.log so failures are inspectable, never
surfaced as hook output - this must never block or fail the agent's turn)
```

Debounce reuses `watch.py`'s own constants/thresholds (3s) rather than inventing new ones, so the two
code paths (persistent `watch`, hook-triggered `update`) behave identically if a user runs both.

## Logical Design

**New CLI surface**: none required β€” this wires the hook installer to an *existing* command
(`graphify update <path>`), it does not add a new subcommand.

**Changed files**:
- `graphify/__main__.py` β€” add `_POST_EDIT_HOOK` (or similarly-named constant, matching
`_SETTINGS_HOOK`/`_READ_SETTINGS_HOOK`'s existing naming) with `"matcher": "Edit|Write|MultiEdit"`, a
`PostToolUse` hook body following the same `python3 -c "..."` stdin-parsing convention. Register it in
`_install_claude_hook` alongside the two existing `PreToolUse` entries (add a `hooks["PostToolUse"]`
key). Update `_uninstall_claude_hook` symmetrically. Fix the now-true printed claim at lines 1980-1981
(no wording change needed once this actually ships β€” currently false, becomes true).
- `graphify/watch.py` β€” expose the debounce-and-spawn logic as an importable function (e.g.
`trigger_background_update(project_root: Path, debounce: float = 3.0)`) rather than duplicating
`_rebuild_lock`'s PID-file logic inline in a shell one-liner; the hook's shell command calls
`python3 -m graphify.watch --trigger <path>` (or similar thin CLI entry) which imports and calls it β€”
keeps the actual logic in Python (testable) rather than growing the shell-embedded hook string.
- Docs: README's install/hook section (mentions `PreToolUse` hooks already) gets a line for the new
`PostToolUse` behavior; CHANGELOG entry under `## Unreleased`.

**Config surface**: none new by default (auto-installed alongside the existing hooks on `graphify
install`/`graphify claude install`). If real usage shows false positives (rebuild firing on files that
don't matter, e.g. scratch files), add an opt-out flag then β€” not speculatively now.

**Test plan** (mirrors this session's own verification discipline β€” real repo, not just synthetic):
1. Unit: hook JSON structure (matcher, command presence) β€” same shape as existing tests for
`_SETTINGS_HOOK`/`_READ_SETTINGS_HOOK` if any exist (check `tests/test_*hook*.py` first).
2. Unit: `trigger_background_update`'s debounce/PID-lock logic in isolation (tmp_path graph, monkeypatched
subprocess spawn β€” do NOT actually spawn a real background process in the test suite).
3. Live validation: install the hook in a real scratch project, make an Edit-tool-shaped change (or
simulate via the same stdin-JSON the hook parses), confirm `graph.json`'s mtime/node count actually
changes without a manual `graphify update` call β€” same "validate against the real failing case, not
just a synthetic test" standard this session held itself to for the query/affected/HTML fixes.

## Non-goals (restated from P17, now with reasoning grounded in the orientation above)

- No Merkle-tree/network sync (Cursor's design) β€” solves a cloud-embedding-consistency problem graphify
doesn't have.
- No change to `graphify watch` itself β€” it stays as the correct answer for "a human wants a live-updating
graph while editing in an IDE," a different use case from what this design closes.
- No new optional dependency β€” deliberately avoided `watchdog` for this path so the fix ships for every
install, not just ones that opted into the `watch` extra.
Loading