From d9aa11087e7cd9c25ce23719c739895df98f2a4c Mon Sep 17 00:00:00 2001 From: Chad El Kurdi <162061136+Chad-Mufasax@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:38:41 +0200 Subject: [PATCH 1/4] graph + nightly: basename link resolution, off-machine vault backup, sync/embed split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - graph: every skeleton dir (Team/, Agents/, Decisions/, Skills/, Journal/…) is outside gbrain's entity-dir whitelist, so wikilinks were silently dropped and the graph stayed empty. install.sh sets link_resolution.global_basename=true for new installs; the nightly sets it idempotently so existing installs pick it up. Skeleton CLAUDE.md files document the convention: bare-basename wikilinks + a See also per page. Field-tested 2026-07-23 on a 562-page vault: 0 -> 185 edges. - nightly: best-effort git push of the vault after the nightly commit (local commits are worth little if the disk dies); never blocks the cycle. - nightly: split sync (--no-embed) + embed --stale — sync's inline embed path fails against zembed-1 with a misleading parse error and silently stops ingesting the vault; split, both phases pass. Claude-Session: https://claude.ai/code/session_013ERfM4utJGsw4McARGJt6a --- company-skeleton/CLAUDE.md | 1 + engine/nightly/gbrain-nightly.sh | 23 ++++++++++++++++++++++- install.sh | 7 +++++++ vault-skeleton/CLAUDE.md | 13 +++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/company-skeleton/CLAUDE.md b/company-skeleton/CLAUDE.md index 575fa7a..a6cff4e 100644 --- a/company-skeleton/CLAUDE.md +++ b/company-skeleton/CLAUDE.md @@ -25,5 +25,6 @@ Each member's nightly `git pull`s this vault + re-indexes. The dream cycle dedup ## Rules - **Zero secrets** (shared vault + on GitHub). Locations only. +- Link related pages with bare-basename wikilinks (`[[Page-Name]]`, no path, no `.md`) — that's what builds the shared graph. End substantial pages with a short `## See also`. - 1 decision = 1 dated file in `Decisions/` (`YYYY-MM-DD-topic.md`), clear title, the "why". - Factual, no personal drafts (those go in your personal vault `~/Documents/Brain`). diff --git a/engine/nightly/gbrain-nightly.sh b/engine/nightly/gbrain-nightly.sh index cbab3e4..c1a59b0 100755 --- a/engine/nightly/gbrain-nightly.sh +++ b/engine/nightly/gbrain-nightly.sh @@ -63,13 +63,34 @@ if [ -d "$GSTACK/.git" ] && cd "$GSTACK" 2>/dev/null; then fi fi +# 0ter. Link-graph resolution: idempotent, upgrades installs that predate the +# flag (see install.sh — without it every skeleton dir is outside gbrain's +# entity-dir whitelist and wikilinks are silently dropped: empty graph). +"$GBRAIN" config set link_resolution.global_basename true >> "$LOG" 2>&1 + # 1. Commit the personal vault first (sync is git-diff based → without a commit, edits are invisible). if cd "$VAULT" 2>/dev/null && [ -n "$(git status --porcelain 2>/dev/null)" ]; then git add -A >> "$LOG" 2>&1 git -c user.email="brain@local" -c user.name="brain" commit -q -m "nightly $(date '+%Y-%m-%d')" >> "$LOG" 2>&1 \ && echo "[git] personal vault committed" >> "$LOG" fi -"$GBRAIN" sync --repo "$VAULT" --no-pull >> "$LOG" 2>&1 +# 1bis. Back up the vault off-machine. The vault is the only copy otherwise +# (local commits aren't worth much if the disk dies). Best-effort: a remote may +# be absent and a cron may lack creds — never block the cycle. +# GIT_TERMINAL_PROMPT=0 so a missing credential fails fast instead of hanging. +if cd "$VAULT" 2>/dev/null && git remote get-url origin >/dev/null 2>&1; then + GIT_TERMINAL_PROMPT=0 git push origin HEAD >> "$LOG" 2>&1 \ + && echo "[git] vault pushed to origin" >> "$LOG" \ + || echo "[git] vault push skipped (no remote creds in cron?)" >> "$LOG" +fi +# Import and embed are split on purpose (2026-07-16). `sync` with its built-in +# embed fails on most files with "[embed(zeroentropyai:zembed-1)] Invalid JSON +# response", reported as "N file(s) failed to parse" — a lie: the parse is fine. +# Only sync's inline embed path fails; the same texts embed cleanly on their +# own. Left unsplit, the RAG silently stops ingesting the vault for weeks. +# Re-test `sync` alone after a gbrain upgrade; drop this split once fixed. +"$GBRAIN" sync --repo "$VAULT" --no-pull --no-embed >> "$LOG" 2>&1 +"$GBRAIN" embed --stale >> "$LOG" 2>&1 # 2. COMPANY vault (team mode): pull teammates' contributions + sync the 'company' source. if [ -d "$VAULT_CO/.git" ]; then diff --git a/install.sh b/install.sh index f2ab4ff..1964efa 100755 --- a/install.sh +++ b/install.sh @@ -164,6 +164,13 @@ os.chmod(p, 0o600) print(" ok ZE key written (config.json, 600)") PY "$GBQ_BIN" config set search.mode balanced >/dev/null 2>&1 || true +# Link graph: gbrain's extractor only auto-recognizes English "entity" dirs +# (people/, companies/, projects/…). EVERY skeleton dir (Team/, Agents/, +# Decisions/, Skills/, Journal/…) is outside that whitelist, so without this +# flag all wikilinks are silently dropped and the graph stays empty forever. +# Basename resolution links [[Page-Name]] to the page whose filename matches +# (case-insensitive), regardless of folder. +"$GBQ_BIN" config set link_resolution.global_basename true >/dev/null 2>&1 || true "$GBQ_BIN" import "$BRAIN" --no-embed >/dev/null 2>&1 && ok "vault imported" say "Embedding (may take 1-2 min)…" "$GBQ_BIN" embed --stale >/dev/null 2>&1 && ok "embedded" || warn "re-run embed: gbrain embed --stale" diff --git a/vault-skeleton/CLAUDE.md b/vault-skeleton/CLAUDE.md index 7aec25c..ccd2977 100644 --- a/vault-skeleton/CLAUDE.md +++ b/vault-skeleton/CLAUDE.md @@ -55,6 +55,19 @@ Always use `gbq` — it's the safe universal wrapper for every gbrain command (r | `Clients/` | Actifs/, Prospects/ | | `Resources/` | Templates/, reusable | +## Wikilinks — how the graph gets built + +Link pages with **bare-basename wikilinks**: `[[Page-Name]]` — no folder path, no +`.md`, case-insensitive. The nightly extract turns them into graph edges +(`gbq backlinks ` to read them back). Path-qualified links +(`[[Projects/Page-Name]]`) break resolution on some write paths — avoid them. + +When you write or update a page, end it with a short `## See also` section +(2-5 wikilinks to genuinely related pages, one short reason each). A page +nobody links to is invisible to graph traversal. Few good links > many weak. + +--- + ## Routing — where to write - Correction → `Profile/lessons.md` (Rule #1) - Meaningful decision worth answering "why X?" in 6 months → `Decisions/YYYY-MM-DD-.md` From aff10657eb755309c6f362cf45db8b42e29e545c Mon Sep 17 00:00:00 2001 From: Chad El Kurdi <162061136+Chad-Mufasax@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:40:23 +0200 Subject: [PATCH 2/4] lint: weekly verify-and-surface job (Monday 08:00) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nightly maintains the brain but reports to a log nobody reads — a silently-failing maintenance job looks exactly like a healthy one. The weekly lint runs doctor / lint / orphans / anomalies / back-links / stats, checks the nightly actually ran in the last 48h, and surfaces a 🟢/🟠/🔴 verdict where it gets read: full report in Profile/lint.md, one-line block pinned in Profile/memory.md. Pure CLI, no LLM pass — immune to transient API failures. Claude-Session: https://claude.ai/code/session_013ERfM4utJGsw4McARGJt6a --- .../com.USER.gbrain-lint.plist.template | 28 +++ engine/nightly/gbrain-lint.sh | 178 ++++++++++++++++++ install.sh | 6 +- 3 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 engine/nightly/com.USER.gbrain-lint.plist.template create mode 100644 engine/nightly/gbrain-lint.sh diff --git a/engine/nightly/com.USER.gbrain-lint.plist.template b/engine/nightly/com.USER.gbrain-lint.plist.template new file mode 100644 index 0000000..62b1ede --- /dev/null +++ b/engine/nightly/com.USER.gbrain-lint.plist.template @@ -0,0 +1,28 @@ + + + + + Label + com.__USER__.gbrain-lint + ProgramArguments + + /bin/zsh + __HOME__/.claude/hooks/brain/gbrain-lint.sh + + StartCalendarInterval + + Weekday + 1 + Hour + 8 + Minute + 0 + + RunAtLoad + + StandardOutPath + __HOME__/.gbrain/lint.launchd.out + StandardErrorPath + __HOME__/.gbrain/lint.launchd.err + + diff --git a/engine/nightly/gbrain-lint.sh b/engine/nightly/gbrain-lint.sh new file mode 100644 index 0000000..5deed92 --- /dev/null +++ b/engine/nightly/gbrain-lint.sh @@ -0,0 +1,178 @@ +#!/bin/zsh +# brain-in-a-box weekly lint (launchd: com..gbrain-lint, Monday 08:00). +# +# The nightly MAINTAINS the brain (sync/dream) but writes to a log nobody +# reads — a silently-failing maintenance job looks exactly like a healthy one. +# This job VERIFIES and surfaces the result where it gets read: +# - /Profile/lint.md : full report (replaced every run) +# - /Profile/memory.md : one-line verdict between markers +# No LLM pass — pure CLI, immune to transient API failures. Read-only on the +# vault except those two files. + +export PATH="$HOME/.bun/bin:/opt/homebrew/bin:/usr/bin:/bin" +GBRAIN="$HOME/.bun/bin/gbrain" +VAULT="$HOME/Documents/Brain" +LOG="$HOME/.gbrain/lint.log" +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT + +# doctor is cwd-sensitive (its resolver check targets the vault's skills) +cd "$VAULT" || exit 1 + +echo "===== $(date '+%F %T') lint start =====" >> "$LOG" + +# last week's report quotes raw issues → lint would recount them; remove it +# before the pass (regenerated at the end of the run) +rm -f "$VAULT/Profile/lint.md" + +"$GBRAIN" doctor --fast --json > "$TMP/doctor.json" 2>>"$LOG" +"$GBRAIN" lint "$VAULT" > "$TMP/lint.txt" 2>&1 +"$GBRAIN" orphans --count > "$TMP/orphans.txt" 2>>"$LOG" +"$GBRAIN" anomalies --since 7d > "$TMP/anomalies.txt" 2>&1 +"$GBRAIN" check-backlinks check "$VAULT" > "$TMP/backlinks.txt" 2>&1 +"$GBRAIN" stats > "$TMP/stats.txt" 2>&1 +grep -E 'nightly (start|done)' "$HOME/.gbrain/nightly.log" 2>/dev/null | tail -6 > "$TMP/nightly.txt" + +python3 - "$TMP" "$VAULT" <<'PY' >> "$LOG" 2>&1 +import json, re, sys +from datetime import datetime +from pathlib import Path + +tmp, vault = Path(sys.argv[1]), Path(sys.argv[2]) +today = datetime.now().strftime("%Y-%m-%d") + +def read(name): + p = tmp / name + return p.read_text(errors="replace") if p.exists() else "" + +# --- doctor --- +doctor_status, doctor_score, doctor_fails = "unknown", "?", [] +try: + d = json.loads(read("doctor.json")) + doctor_status = d.get("status", "unknown") + doctor_score = d.get("health_score", "?") + doctor_fails = [c["name"] for c in d.get("checks", []) if c.get("status") == "fail"] +except Exception: + pass + +# --- lint --- +lint_txt = read("lint.txt") +m = re.search(r"(\d+) pages scanned\. (\d+) issue\(s\)", lint_txt) +lint_pages, lint_issues = (int(m.group(1)), int(m.group(2))) if m else (0, -1) + +# --- orphans --- +m = re.search(r"^(\d+)$", read("orphans.txt"), re.M) +orphans = int(m.group(1)) if m else -1 + +# --- anomalies / backlinks --- +anom_txt = read("anomalies.txt").strip() +anomalies_clean = "(no anomalies" in anom_txt or not anom_txt +back_txt = read("backlinks.txt") +backlinks_clean = "No missing back-links" in back_txt + +# --- nightly freshness --- +nightly_txt = read("nightly.txt") +last_done, nightly_age_h = None, None +for line in nightly_txt.splitlines(): + m = re.search(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) nightly done", line) + if m: + last_done = m.group(1) +if last_done: + nightly_age_h = (datetime.now() - datetime.strptime(last_done, "%Y-%m-%d %H:%M:%S")).total_seconds() / 3600 + +# --- stats --- +stats_txt = read("stats.txt").strip() +m = re.search(r"Links:\s+(\d+)", stats_txt) +links = int(m.group(1)) if m else -1 +m = re.search(r"Pages:\s+(\d+)", stats_txt) +db_pages = int(m.group(1)) if m else -1 + +# --- verdict --- +red = (nightly_age_h is None or nightly_age_h > 48) or (isinstance(doctor_score, int) and doctor_score < 50) or lint_issues < 0 +orange = doctor_status != "healthy" or lint_issues > 0 or not backlinks_clean or not anomalies_clean +verdict = "🔴" if red else ("🟠" if orange else "🟢") + +reasons = [] +if nightly_age_h is None: + reasons.append("nightly: no 'done' entry found in the log") +elif nightly_age_h > 48: + reasons.append(f"nightly DEAD for {nightly_age_h:.0f}h") +else: + reasons.append(f"nightly OK (last run {last_done})") +reasons.append(f"doctor {doctor_status} ({doctor_score}/100" + (f", fail: {', '.join(doctor_fails)}" if doctor_fails else "") + ")") +if lint_issues >= 0: + reasons.append(f"{lint_issues} frontmatter/artifact issue(s) across {lint_pages} pages") +reasons.append(f"{orphans} orphan pages / {db_pages} in DB, {links} links") +if not anomalies_clean: + reasons.append("statistical anomalies detected (see report)") +if not backlinks_clean: + reasons.append("missing back-links (see report)") + +summary = " · ".join(reasons) + +# --- full report -> Profile/lint.md --- +report = f"""--- +title: Weekly brain lint +created: {today} +type: report +--- + +# Weekly lint — {today} {verdict} + +{summary} + +Generated by engine/nightly/gbrain-lint.sh (launchd, Monday 08:00). +Raw output of each pass below. + +## doctor --fast +```json +{read("doctor.json").strip()} +``` + +## lint (frontmatter, LLM artifacts, placeholder dates) +Bulk-fixable: `gbrain lint --fix` +``` +{lint_txt.strip()[-4000:]} +``` + +## anomalies (7 days) +``` +{anom_txt or "(empty)"} +``` + +## back-links +``` +{back_txt.strip()[-1500:]} +``` + +## stats +``` +{stats_txt} +``` + +## nightly (recent runs) +``` +{nightly_txt.strip() or "(nightly log not found)"} +``` +""" +(vault / "Profile" / "lint.md").write_text(report) + +# --- verdict block -> Profile/memory.md (between markers, idempotent) --- +mem_path = vault / "Profile" / "memory.md" +block = ( + "\n" + f"> **🧹 Weekly lint {today} {verdict}** — {summary}. Detail: `Profile/lint.md`.\n" + "" +) +mem = mem_path.read_text() if mem_path.exists() else "# Memory\n" +if "" in mem: + mem = re.sub(r".*?", block, mem, flags=re.S) +else: + lines = mem.splitlines(keepends=True) + lines.insert(min(2, len(lines)), "\n" + block + "\n") + mem = "".join(lines) +mem_path.write_text(mem) + +print(f"lint {verdict} — {summary}") +PY + +echo "===== $(date '+%F %T') lint done =====" >> "$LOG" diff --git a/install.sh b/install.sh index 1964efa..232dea2 100755 --- a/install.sh +++ b/install.sh @@ -84,9 +84,10 @@ say "gbq + nightly" mkdir -p "$BIN" sed "s#__HOME__#$H#g" "$REPO/engine/bin/gbq" > "$BIN/gbq" && chmod +x "$BIN/gbq"; ok "gbq → $BIN/gbq" sed "s#__HOME__#$H#g" "$REPO/engine/nightly/gbrain-nightly.sh" > "$HOOKS/gbrain-nightly.sh" && chmod +x "$HOOKS/gbrain-nightly.sh"; ok "gbrain-nightly.sh" +sed "s#__HOME__#$H#g" "$REPO/engine/nightly/gbrain-lint.sh" > "$HOOKS/gbrain-lint.sh" && chmod +x "$HOOKS/gbrain-lint.sh"; ok "gbrain-lint.sh" -# ── 7. launchd jobs (nightly maintenance + daily reflection) ───────────────── -say "launchd (nightly 04:00 + reflection 12:00/23:00)" +# ── 7. launchd jobs (nightly maintenance + daily reflection + weekly lint) ─── +say "launchd (nightly 04:00 + reflection 12:00/23:00 + lint Monday 08:00)" mkdir -p "$H/Library/LaunchAgents" load_agent() { #