Add vault_sync.py hook; complete Step 5b and 2e pilot

Step 5b (Obsidian vault -> VPS git sync) is now fully done: the vault is a
private Forgejo repo (SecondBrain) and vault_sync.py auto-commits+pushes it on
SessionEnd, mirroring memsearch_sync.py. Step 2e pilot done (llf-schema project
graph onboarded). Step 2d closed (live vault graph is the baseline). Docs and
implementation status updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jared 2026-06-15 15:23:32 -04:00
parent fa639cfad7
commit 7ede69ec85
3 changed files with 90 additions and 12 deletions

View File

@ -67,10 +67,12 @@ to those two and fix the stale doc.
**Implementation status (2026-06-12):** Memory plugin source moved into git at `cc-os/plugins/memory/` and bash hooks ported to Python (deep-module architecture: shared `config.py`, `hook_io.py`, `session_state.py`; thin entry-point scripts). Cutover via symlink `~/.claude/plugins/memory → cc-os/plugins/memory/` and settings.json hook rewrite. memsearch sync split into dedicated `memsearch_sync.py` SessionEnd hook (relocation of ADR-015 behavior, not reversal). Fresh-session test passed 2026-06-12. See ADR-016.
**Implementation status (2026-06-15):** Step 5b done and automated — vault (`~/Documents/SecondBrain`) initialized as git repo, pushed to private Forgejo (`ssh://git@forgejo.swansoncloud.com:2222/jared/SecondBrain.git`); 52 files, git chosen over Syncthing. Auto-commit+push wired via `vault_sync.py` SessionEnd hook (third SessionEnd hook, runs after `session_end.py` so the daily journal note is included; push-only — SessionStart pull is an optional future item for multi-machine). Step 2e pilot done — llf-schema (`/home/jared/dev/llf-schema`, PHP 8.2 WordPress plugin) onboarded via `memory-project` skill: 605 nodes / 930 edges / 52 communities; Ollama doc pass lossy on WordPress docs but AST pass solid. Step 2d closed — live vault `graphify-out/` is the baseline; fixture-only build superseded.
## Implemented Components
**Global memory plugin** — `cc-os/plugins/memory/` (git-tracked, 2026-06-12); symlinked into `~/.claude/plugins/memory`
- Hooks: `hooks/``session_start.py`, `session_context.py` (project graph path only), `post_tool_use_write.py`, `session_end.py` (vault journal), `memsearch_sync.py` (second SessionEnd hook; memsearch auto-commit+push, 30s timeout)
- Hooks: `hooks/``session_start.py`, `session_context.py` (project graph path only), `post_tool_use_write.py`, `session_end.py` (vault journal), `memsearch_sync.py` (second SessionEnd hook; memsearch auto-commit+push, 30s timeout), `vault_sync.py` (third SessionEnd hook; vault auto-commit+push to forgejo.swansoncloud.com/jared/SecondBrain, 30s timeout; mirrors memsearch_sync.py)
- Shared modules: `config.py` (load_config → frozen Config dataclass), `hook_io.py` (read_input → HookInput dataclass), `session_state.py` (record_touch/read_touches; encapsulates `/tmp/claude-vault-touched-$SESSION_ID` contract)
- Skills: `skills/` — memory-vault, memory-write, memory-reorganize, memory-project
- Config: `config.yaml` — vault path, Ollama model (qwen25-coder-7b-16k), env vars
@ -89,7 +91,12 @@ to those two and fix the stale doc.
- Skills: `/memory-recall`, `/memory-config` (ship with plugin)
- Git sync: `~/.memsearch` is a dedicated private Forgejo repo (`forgejo.swansoncloud.com/jared/memsearch`); whitelist `.gitignore` commits only `memory/*.md` (excludes rebuildable `milvus.db`, model, config); auto-commit+push wired into the cc-os memory plugin's own `memsearch_sync.py` SessionEnd hook (relocated from `session-end.sh` by ADR-016, 2026-06-12; behavior preserved — see ADR-015)
**Not yet implemented:** Obsidian vault → VPS sync (Step 5b). Memsearch episodic git sync is done (Step 5a, 2026-06-09).
**Obsidian vault git sync** — `~/Documents/SecondBrain` (2026-06-15)
- Remote: `ssh://git@forgejo.swansoncloud.com:2222/jared/SecondBrain.git` (private Forgejo; web: `https://forgejo.swansoncloud.com/jared/SecondBrain`)
- 52 files tracked (notes, journal, templates, vault-conventions.md, CLAUDE.md, .obsidian config); `graphify-out/` excluded via `.gitignore`
- Auto-commit+push via `vault_sync.py` SessionEnd hook in cc-os memory plugin (mirrors `memsearch_sync.py`; push-only — SessionStart pull is optional future item for multi-machine)
**Remaining optional items:** SessionStart vault pull (multi-machine sync; push-only is the current design); additional project onboarding (one at a time, per ADR-013); bulk vault migration. All required build steps complete as of 2026-06-15.
## OpenSpec workflow

View File

@ -1,6 +1,6 @@
# Build Plan
_Last updated: 2026-06-12_
_Last updated: 2026-06-15_
How a human builds this system, step by step, and answers to the operational questions:
which scripts and hooks, how the AI knows when to write and what conventions to follow, how and
@ -142,8 +142,10 @@ tools, clients, and domain concepts).
**Note (2026-06-08):** The `~/Documents/SecondBrain` vault already has a live `graphify-out/`
graph (32 files, built 2026-06-05). The fixture-first approach from ADR-013 may therefore be
superseded — the vault graph exists before Step 1 (conventions/fixtures) is formally complete.
Determine whether to treat the existing vault graph as the fixture baseline or run a clean
fixture-only build once Step 1 is done. This is an open question before 2d can be marked done.
**Resolved (2026-06-15): DONE/superseded.** The live vault `graphify-out/` over the full vault
IS the baseline graph. A fixture-only build is moot now that the vault holds real scaffolding
notes (not just fixtures). No separate fixture-only build will be run.
**Full vault migration** (the `~/Documents/SecondBrain` build above run over all notes) is the final step —
deferred to after end-to-end validation on the pilot project. Do not bulk-migrate the vault
@ -168,7 +170,13 @@ Keep per-project graphs separate — do not merge client projects into one graph
but `cc-os` is a docs-only repo, not a pilot project in the ADR-013 sense. A pilot project
with real code has not yet been onboarded.
**Status: NOT STARTED** (pilot project onboard pending)
**Status: PILOT DONE (2026-06-15).** The llf-schema project (`/home/jared/dev/llf-schema`,
PHP 8.2 WordPress plugin) was onboarded via the `memory-project` skill: `graphify extract`
(tree-sitter AST on 90 code files + Ollama doc pass on 9 doc files, ~3.5 min) + `cluster-only`
→ 605 nodes / 930 edges / 52 communities (~524K `graph.json`). `graphify-out/` added to that
repo's `.gitignore`. **Finding:** Ollama doc pass was lossy on structured WordPress docs (101
warnings, recursive bisection) but the AST pass carried the code structure — acceptable for
code-heavy repos. **Remaining under 2e:** onboard additional projects incrementally.
### Step 3 — Hooks (maintenance + retrieval) **DONE (2026-06-08)**
@ -216,11 +224,24 @@ subshell with trailing `|| true` — cannot fail session shutdown. See **ADR-015
and the cross-client commingling, which is an accepted design choice (single global store
across all clients by intent — see ADR-015, resolved 2026-06-09).
#### 5b — Obsidian vault → VPS sync — **NOT STARTED**
- Pick **git** (versioned, hourly push/pull) or **Syncthing** (continuous, zero-thought) for
the vault → VPS. Configure on each machine.
- **Do not sync** the Graphify `graphify-out/` directories, Milvus caches, or the Ollama
models. Graphs are rebuilt per machine from the vault (the single source of truth).
#### 5b — Obsidian vault → VPS sync — **DONE (2026-06-15)**
The vault at `~/Documents/SecondBrain` was initialized as a git repo and pushed to a new
private Forgejo repo (`ssh://git@forgejo.swansoncloud.com:2222/jared/SecondBrain.git`;
web: `https://forgejo.swansoncloud.com/jared/SecondBrain`), mirroring the memsearch sync
pattern. 52 files committed on 2026-06-15 (notes, journal, templates, vault-conventions.md,
CLAUDE.md, .obsidian config); `graphify-out/` correctly excluded via the existing `.gitignore`.
**Decision:** git sync chosen over Syncthing.
**Auto-sync hook — DONE (2026-06-15):** `vault_sync.py` added as the third SessionEnd hook
(after `session_end.py` and `memsearch_sync.py`, timeout 30). Auto-commits `git add -A` and
pushes the vault to `forgejo.swansoncloud.com/jared/SecondBrain` on every session end, so the
daily journal note from `session_end.py` is included in the commit. Mirrors the
`memsearch_sync.py` pattern; uses `cfg.vault_path`. Smoke-tested (exit 0) 2026-06-15.
**Remaining (optional):** SessionStart `git pull` for multi-machine freshness — push-only today.
**Do not sync** the Graphify `graphify-out/` directories, Milvus caches, or the Ollama
models. Graphs are rebuilt per machine from the vault (the single source of truth).
### Step 6 — Package as a global plugin (Part D) **DONE (2026-06-08)**
- Wrap Steps 23 into a Claude Code plugin with skills; install at user level.
@ -358,7 +379,7 @@ global install keeps conventions a single source of truth.
1. **Vault location** — Settled by ADR-012: `~/Documents/SecondBrain`. Symlink into
`~/.claude/memory` only if a tool requires it.
2. **Sync mechanism** — git (versioned, hourly) vs Syncthing (continuous)?
2. **Sync mechanism**~~git (versioned, hourly) vs Syncthing (continuous)?~~ **RESOLVED (2026-06-15): git.**
3. **Stale rebuild threshold** — how many days before SessionStart triggers `--force`?
7 days is the starting guess; tune after observing drift in practice.
4. **Per-project graph path** — how does the AI know which `graph.json` to pass to

View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""SessionEnd hook — auto-commit + push the SecondBrain vault.
Step 5b automation: commit vault content and push to
forgejo.swansoncloud.com/jared/SecondBrain. Mirrors memsearch_sync.py.
Must run AFTER session_end.py so the daily journal note that hook writes is
included in the commit. The disposable graphify-out/ index is excluded via the
vault's .gitignore. Fail-open; must never disrupt session shutdown.
"""
import os
import sys
import subprocess
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import config as config_mod
def main():
cfg = config_mod.load_config()
vault_dir = cfg.vault_path
if not os.path.isdir(os.path.join(vault_dir, ".git")):
return
devnull = subprocess.DEVNULL
subprocess.run(["git", "-C", vault_dir, "add", "-A"], stdout=devnull, stderr=devnull)
staged = subprocess.run(
["git", "-C", vault_dir, "diff", "--cached", "--quiet"],
stdout=devnull, stderr=devnull,
)
if staged.returncode != 0:
date_str = datetime.now().strftime("%Y-%m-%d")
subprocess.run(
["git", "-C", vault_dir, "commit", "-m", f"vault: session notes {date_str}"],
stdout=devnull, stderr=devnull,
)
print("[session-end] vault committed")
subprocess.run(
["timeout", "30", "git", "-C", vault_dir, "push", "--quiet", "origin", "main"],
stdout=devnull, stderr=devnull,
)
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"[vault-sync] error: {e}", file=sys.stderr)
sys.exit(0)