joecastelino

jay-brain-and-skill-index

Operate, diagnose, and repair Jay's GBrain (the single Jay-only knowledge brain) plus the self-maintaining skill index. Use when the nightly brain refresh fails or looks frozen, when `gbrain` is "command not found", when GBrain search returns nothing/stale, when session knowledge needs to be captured into the brain so it's retrievable later, or when the skill index needs descriptions/metadata/usage-counts. Covers the bun-PATH break, the multi-brain split, the embedding-key gotcha, and the capture-session / skill-usage tracking scripts.

joecastelino 2 1 Updated 5d ago

Resources

1
GitHub

Install

npx skillscat add joecastelino/jay-skill-pack/jay-brain-and-skill-index

Install via the SkillsCat registry.

SKILL.md

Jay's Brain + Skill Index — Operate & Repair

HOW GBRAIN ACTUALLY WORKS — use its built-ins, don't reinvent (clarified 2026-06-25)

GBrain (v0.42) owns the whole pipeline: chunk → embed → store → retrieve → maintain. My job is to
feed it clean, well-formed pages and USE THE RIGHT COMMANDS, not rebuild what it does.

  • Retrieval — use gbrain query "<question>" (a.k.a. ask), NOT search. query = HYBRID
    search (vector embeddings + RRF reciprocal-rank-fusion + query expansion) → the real "retrieve
    data nodes" tool. search is only tsvector keyword match (weaker; readme/Obsidian page often
    floats to top on broad keyword searches — ignore it, it's the readme text quoted in a skill page).
  • Chunking/embedding are automatic on import/sync. ZeroEntropy zembed-1, 1280-dim, per
    chunk. Pages >~50KB trigger a "consider splitting" WARN (not error) — chunk long transcripts.
  • gbrain link SLUG + FLAG GOTCHA (verified 2026-06-25): to create an explicit edge use
    gbrain link <from> <to> --link-type references. The target slug MUST include its category
    prefix — projects/session-20260625_075211_dce000, NOT the bare session-2026... (bare fails
    with addLink failed: page "..." not found). The root hub page is just index (no prefix). The
    ONLY valid options are --link-type and --context; there is NO --link-source flag (the
    brain-sync cron prompt suggests --link-source sync — it's silently ignored/causes the not-found,
    so drop it). Linking the 2 nightly session captures from index nudged Links 174→178.
  • LOADING THIS SKILL IN A CRON RUN — skill_view TRUNCATES IT (confirmed 2026-08-18): SKILL.md is
    large, so skill_view('jay-brain-and-skill-index') can come back cut off ("Truncated: tool response
    was 102,475 chars") and the cron happy-path section is NOT in what you get. Don't hunt for the file
    with find / (timed out at 180s in this profile). Go straight to
    /home/itadmin/.hermes/profiles/jay/skills/automotive/jay-brain-and-skill-index/SKILL.md
    (mirror at /home/itadmin/jay-skill-pack/skills/automotive/...) and read_file(path, offset=35, limit=120) — the "Nightly brain-sync cron normal path" block is around lines 35-77. ALSO:
    search_files(..., context=60) called from inside execute_code raises
    json.decoder.JSONDecodeError: Extra data (multi-doc output breaks the hermes_tools JSON parse) —
    use the NATIVE search_files/read_file tools for big-context reads, not the execute_code wrappers.
  • RE-IMPORT CHUNK COUNT IS NOT AN ERROR (2026-08-18): the post-commit re-import reports something
    like "1 page imported / 23 chunks created" for index.md, yet stats total Chunks does not grow —
    index.md's old chunks are REPLACED, not added. Embedded==Chunks still holds. Don't chase this.
  • Nightly brain-sync cron normal path (2026-06-25, re-verified clean 2026-06-26): the every-15-min
    session-end-sync already commits new session pages, so when the brain-sync cron runs, git is usually
    CLEAN and import picks up the 1-2 newest projects/session-* captures ("2 pages imported / 135
    unchanged" = right tree). The "new pages to link from index" are those session captures. FULL
    HAPPY-PATH SEQUENCE (each git cmd a SEPARATE terminal call, no && chains):
    1. gbrain import /home/itadmin/brain/ --no-embed + gbrain embed --stale (auto-embeds new sessions).
    2. Find unlinked: comm -23 <(ls brain/projects/session-*.md|sed 's#.*/##;s#\.md$##'|sort -u) <(gbrain backlinks index|grep -io 'session-[0-9_a-f]*'|sort -u).
    3. For each: verify it exists with gbrain get projects/session-<ts> (head -3 shows frontmatter),
      then gbrain link projects/session-<ts> index --link-type child_of → expect {"status":"ok"}.
      ORPHAN-CLEARING DIRECTION GOTCHA (confirmed 2026-08-18, 2nd brain-sync cron of the day):
      gbrain orphans does NOT consider a page linked just because it has an outbound edge to a hub
      (page→hub, e.g. link projects/session-<ts> index). It only clears once there's an INBOUND edge
      FROM the hub TO the page (hub→page): gbrain link index projects/session-<ts> --link-type references. Proof: ran link session-X index --link-type references, got {"status":"ok"},
      but gbrain orphans still listed session-X; gbrain graph session-X --depth 1 --direction both
      showed only the session→index edge I'd just made. Re-ran as link index session-X --link-type references (args swapped) and orphans dropped to 0 immediately. So when following the cron
      prompt's own correct instruction ("clear orphans with hub→page edges: gbrain link index <slug> --link-type references"), do it exactly that way — hub first, page second — don't default to
      the page→hub child_of pattern from step 3 above for orphan-clearing purposes; that pattern may
      still be fine for build-brain-edges.py's own bulk edge-building logic (untested here), but for a
      manual one-off orphan fix during the cron sync, hub→page references is what actually works.
    4. ALSO add them to index.md's ## Sessions body list (patch after the last session line; pull the
      title from the page's title: frontmatter for the — description suffix).
      PATCH-TOOL INDENTATION GOTCHA (confirmed 2026-08-18): when the old_string/new_string match
      spans the last session line + the following ## Concepts / Reference heading, the patch tool's
      fuzzy matching sometimes inserts a leading space on every new line (- [[session-...]] becomes
      - [[session-...]]). This breaks nothing functionally but is inconsistent with the rest of the
      file. PREVENT IT: make old_string the LAST SESSION LINE ONLY (a single - [[session-...]] line,
      which is unique) and new_string that same line + the new line(s) — never let the match span the
      blank line or the following ## Concepts / Reference heading. Verified clean 2026-08-24 (3 lines
      appended across two passes, zero indentation drift). If you did span the heading, read_file the
      inserted range and fix any leading space with
      sed -i '<start>,<end>s/^ - /- /' /home/itadmin/brain/index.md before committing.
    5. git add -Agit commit -m "brain sync" (separate calls).
    6. RE-IMPORT + EMBED AGAIN so the EDITED index.md page itself gets re-embedded:
      gbrain import --no-embed (reports "1 page imported") + gbrain embed --stale ("1 chunk").
      Skipping this leaves index.md's new wikilinks unembedded until 3 AM.
    7. Verify: gbrain stats Embedded==Chunks; linked-sessions==disk-sessions count; git status clean.
      SELF-REFERENTIAL ORPHAN GOTCHA (confirmed 2026-08-17): the act of running this sync itself gets
      captured as a NEW session page (e.g. "Nightly GBrain Sync Verified...") sometime during/after your
      first re-import+embed pass. Running gbrain orphans again after step 6 will often find ONE more
      orphan — that session page about the sync you're currently doing. This is expected, not a bug: link
      it (gbrain link index projects/session-<ts> --link-type references), add it to index.md's Sessions
      list, commit, then re-import+embed a THIRD time. Keep looping orphans→link→commit→reimport until
      gbrain orphans reports 0 in the same pass as git status clean — don't stop after just one round
      of fixes assuming it's done.
  • THE GRAPH/EDGES ARE A SEPARATE, MOSTLY-EMPTY LAYER (key gotcha): [[wikilinks]] in page
    BODIES and the frontmatter links: field are CONTENT — they are NOT auto-converted into graph
    edges. As of 2026-06-25 the brain had only 42 links across 99 pages and backlinks index = [].
    Real graph edges are created EXPLICITLY via gbrain link <from> <to> [--type T], or sparsely by
    gbrain extract --stale (LLM semantic edge extraction — conservative, pulled just 3 mentions
    links from 74 pages; it does NOT parse wikilink syntax). So gbrain graph <slug> / backlinks
    / graph-query only return useful nodes for pages where edges were explicitly built. DECISION:
    rely on query (hybrid embedding) for retrieval — it works great and needs no graph. Only invest
    in link edges if a specific traversal/relationship view is wanted. Don't assume wikilinks=edges.

GRAPH EDGES NOW WIRED + AUTO-MAINTAINED (2026-06-25, Joe: "hook up graph traversal, use the brain
to its full advantage"):
/home/itadmin/bin/build-brain-edges.py deterministically creates the
edge layer via gbrain link <from> <to> --link-type T (NOTE flag is --link-type, NOT --type):
every skills/* → skills-index (child_of), kb/* → kb-index (child_of), memory/* + projects/session-* +
concepts/* + the hubs → index (child_of), PLUS topic cross-links skill/concept ↔ kb article
(related_to) matched on keyword sets (source-code, service-menu, opcode, stock-order, parts,
scheduling, process-automation). Idempotent: checks existing backlinks <target> before creating,

  • a belt/suspenders state file /home/itadmin/.gbrain/brain-edges.state. Result 2026-06-25: brain
    went 42→178 links; gbrain graph index --depth 2 --direction both reaches 189 nodes; skills-index
    has 38 clean backlinks (0 dupes). TRAVERSAL USAGE: gbrain graph <slug> --depth N --direction both (CRITICAL: edges are built child→hub, so from a HUB you must use --direction both or in
    to walk DOWN to children; default direction only walks outbound/up). gbrain backlinks <slug> =
    inbound. WIRED INTO PIPELINE: session-end-sync.sh calls build-brain-edges.py after the
    skill-manifest rebuild whenever it captured new pages — so new pages auto-get their edges every
    15-min pass. Edges live in the brain DB (PGLite), NOT the markdown repo (nothing to git-commit).
    Semantic gbrain extract --stale is slow + low-yield (3 links from 74 pages) — left to 3AM refresh.

BUG in build-brain-edges.py — STALE STATE FILE leaves edges silently unbuilt (found+fixed 2026-06-25):
The script's link() short-circuits on the belt/suspenders state file /home/itadmin/.gbrain/brain-edges.state:
if an edge key <from>\t<to>\t<type> is in _edge_state, it returns False and SKIPS the real
gbrain link subprocess — WITHOUT verifying the edge actually exists in the DB. So if a key got
written to state but the edge never persisted (or was lost), the builder reports edges_created:0
forever while the page stays unlinked. Symptom: gbrain backlinks index | grep session | sort -u | wc -l
< number of projects/session-*.md files, yet build-brain-edges.py creates 0. FIX (manual, reliable):
link the missing edges DIRECTLY, bypassing the script: gbrain link projects/session-<ts> index --link-type child_of.
Find the gap: compare ls brain/projects/session-*.md (strip path+.md) vs the dedup'd backlinks-index
session list (comm -23). 2026-06-25 this left 6 sessions unlinked (15→21 of 21 after manual link).

IMPORT can SKIP a committed page (mtime/hash gap) — page absent from brain despite valid .md (2026-06-25):
A valid session .md (good frontmatter, committed in git) was NOT in the brain — gbrain list didn't show
it and gbrain link <slug> index failed addLink failed: page "..." not found. The dir import had counted
it among "unchanged" and skipped it. FIX: touch /home/itadmin/brain/projects/<file>.md to bump mtime, then
re-run gbrain import /home/itadmin/brain/ --no-embed (it imports as 1 changed page), gbrain embed --stale,
then gbrain link. Single-file import does NOT work (gbrain import <file.md> → "Found 0 markdown files";
import wants a directory). Always sanity-check after sync: session→index edge count == session .md file count.

DETECTOR FALSE-POSITIVE + wikilinks NOW make edges (2026-07-03 evening sync): the classic
disk-vs-backlinks index comm -23 diff flagged session-20260703_150319 as unlinked, but it was
FINE — its only edge is index→page (link_type: references, link_source: "markdown"), which
doesn't appear in backlinks index (that lists edges pointing TO index). So when the comm diff
flags a session, check gbrain backlinks projects/session-<ts> for an inbound index→page edge +
its presence in index.md's Sessions list BEFORE remediating — if both exist and gbrain orphans
is 0, nothing to do. ALSO NOTE: link_source: "markdown" means newer gbrain versions DO create
edges from index.md wikilinks (the older "wikilinks are content, not edges" note is stale for
hub-page wikilinks). Healthy 2026-07-03 evening baseline: 292 pages / 913 chunks / 913 embedded /
661 links / 0 orphans.

ORPHAN DETECTION NEEDS AN INBOUND EDGE — hub→page direction (verified 2026-07-03 dream cycle):
gbrain orphans / the dream-cycle orphans phase counts a page as orphaned unless it has an INBOUND
edge. The usual child→hub link (gbrain link projects/session-<ts> index --link-type child_of)
does NOT clear orphan status — after linking 21 orphans that way, the count stayed 21. What clears
it is the hub→page direction: gbrain link index <slug> --link-type references (or
gbrain link skills-index skills/<name> --link-type references for skill pages). The 2026-07-03
dream run found 21 orphans (all of 2026-07-02's session captures + jay-memory-09 +
skills/bt-tony-menu-rebuild — build-brain-edges had missed them, its stale-state bug again); linking
them index→page took orphans 21→0. When fixing orphans, do BOTH directions if you also want
backlinks-index completeness, but the hub→page edge is the one the orphan scanner needs.
KB-PAGE ORPHANS use the kb-index hub (verified 2026-07-12): a KB auto-ingest batch can drop
DOZENS of kb/svc30-* orphans at once (59 in one run — the ingest wrote the pages but built no
edges). Clear them with gbrain link kb-index kb/<slug> --link-type references (hub→page, same
pattern; a simple while-read loop over the orphan list works — 59/59 ok). Parse the orphan list per
category: gbrain orphans groups output under [kb] / [projects] / etc. headers. Hub mapping:
sessions/memory/concepts → index, skills/* → skills-index, kb/* → kb-index. Latecomer streaks
apply here too — the 15-min session-end-sync's KB ingest can commit NEW kb pages mid-run (one run
had 4 kb + 2 session latecomers after the first orphan pass); loop until gbrain orphans = 0.

SIDE PROJECT QUEUED (Joe wants, later — NOT now): interactive brain VISUALIZATION (node-graph of all
pages + edges, like the Obsidian/online graph views Joe has seen). Build after the brain core is
solid. Data source: gbrain graph/graph-query JSON + list for nodes; render as a force-directed
web graph (own repo per Joe's separate-codebase rule).

(prev) FAILURE #2 note + general:

  • Other built-ins worth knowing: doctor (health: resolver/skills/pgvector/RLS/embeddings),
    graph <slug> --depth N (node traversal, JSON), graph-query <slug> --type --direction,
    backlinks <slug>, timeline, brainstorm/lsd (idea generation over the brain),
    files upload-raw (size-routed file storage), export/import/sync --watch/sync --install-cron.
  • Bottom line for me: stop hand-rolling retrieval; use gbrain query. Keep feeding clean
    embedded pages (the auto-ingest + session-end-sync already do this). The pipeline is GBrain's job.

How Jay's knowledge brain (GBrain) and the skill index work, the recurring failures, and the
scripts that keep them self-maintaining. Joe's intent (2026-06-24): "make sure the brain inside
of Jay is being updated regularly and accurately... actively using it... keeping track of what
skills we're using." Other agents' brains are NOT Jay's concern — only the Jay-specific one.

The architecture (verified 2026-06-24)

  • ONE Jay brain DB: /home/itadmin/.gbrain/brain.pglite (PGLite engine, ZeroEntropy
    zeroentropyai:zembed-1 embeddings, 1280 dims).
  • THREE .gbrain config dirs — this is the #1 source of confusion:
    1. /home/itadmin/.gbrain/ — the itadmin/cron config; its config.json database_path =
      /home/itadmin/.gbrain/brain.pglite. The nightly refresh (cron, HOME=/home/itadmin) uses this.
    2. /home/itadmin/.hermes/profiles/jay/home/.gbrain/Jay's interactive-session config
      (Jay's session HOME = /home/itadmin/.hermes/profiles/jay/home). It has its own config.json
      • .env (the API keys) but NO separate DB — its database_path ALSO points to
        /home/itadmin/.gbrain/brain.pglite. So both read/write the SAME brain. Good — keep it that way.
    3. /home/itadmin/.hermes/profiles/don-ready/home/.gbrain/ — Don Ready's OWN separate DB
      (.../don-ready/home/.gbrain/brain.pglite). Leave it alone — not Jay's.
  • Brain repo (markdown source): /home/itadmin/brain (git, branch master). NOTE this is a
    SHARED fleet vault — it has pages for ALL agents (agents/jay, agents/stacey, agents/walter,
    agents/don, the readme "Obsidian Vault managed by Dori", etc.). Jay-relevant pages: agents/jay,
    system/jay-tool-index, people/joe-castelino, companies/american-motors-group,
    concepts/tekion-, projects/{dealerdetail, caliber-ops, tekion-opcode-pricing, sct-}.
  • Refresh script: /home/itadmin/bin/brain-refresh.sh, cron 0 3 * * *, logs to
    /home/itadmin/.gbrain/brain-refresh.log.

Always-run preamble (env every gbrain command needs)

gbrain is a TypeScript shebang RUN BY bun. bun lives at /home/itadmin/.hermes/node/bin/bun
(it MOVED there — it is NOT in ~/.bun/bin anymore, that dir only has the gbrain symlink).
The embedding key must be exported or sync prints "set ZEROENTROPY_API_KEY" and skips embedding.

export PATH="/home/itadmin/.hermes/node/bin:/home/itadmin/.bun/bin:$PATH"
set -a; source /home/itadmin/.gbrain/.env; set +a   # loads ZEROENTROPY_API_KEY + OPENAI_API_KEY
HOME=/home/itadmin gbrain stats     # always run gbrain with HOME=/home/itadmin (the cron HOME)

FAILURE #1 — refresh silently dead / gbrain: command not found (THE recurring one)

Symptom: brain-refresh.log shows /usr/bin/env: 'bun': No such file or directory then
[warn] sync failed / extract failed / dream failed, or gbrain not on PATH. Root cause: bun
moved to ~/.hermes/node/bin; the script's PATH only had ~/.bun/bin. The || echo warn
swallowed the error so the run "completed" looking healthy. This has broken twice — when paths
got reorganized.
FIX (already applied to brain-refresh.sh, re-apply if it regresses):

export PATH="/home/itadmin/.hermes/node/bin:$HOME/.hermes/node/bin:$HOME/.bun/bin:/home/itadmin/.bun/bin:$PATH"
# plus a fail-LOUD guard at top:
if ! gbrain --version >/dev/null 2>&1; then
  echo "=== $(date -Is) brain-refresh ABORTED: gbrain not runnable (bun? $(command -v bun)) ===" \
    >> /home/itadmin/.gbrain/brain-refresh.log
fi

Verify the fix: HOME=/home/itadmin /usr/bin/bash /home/itadmin/bin/brain-refresh.sh then tail the
log — should end "Brain is healthy. 22 phase(s) checked". bash -n the script first.

FAILURE #1b — sync cron uses ~/brain but ~ is the WRONG tree (silent skip)

The brain-sync cron job runs commands like gbrain import ~/brain/ --no-embed. Under Jay's session
HOME, ~/brain = /home/itadmin/.hermes/profiles/jay/home/brain/ — a STALE 7-file copy, NOT the
canonical 19-file repo at /home/itadmin/brain/. The import reports "0 imported, 0 stale, 0 errors"
and looks perfectly healthy while silently skipping the real brain. Confirmed 2026-06-24: importing
the canonical repo instead picked up 2 changed pages + re-embedded 2 chunks the ~/brain run missed.
WORSE VARIANT (observed 2026-07-02): the stale tree can CLOBBER, not just skip. As of 2026-07-03 the stale
~/brain copy shares exactly TWO slugs with canonical: index and people/joe-castelino
(caliber-ops, tekion-dms, tekion-opcode-pricing, american-motors-group, sessions/session-audit-20260609
are stale-ONLY). So a wrong-tree import reporting "2 pages imported" = it clobbered index +
joe-castelino; the canonical re-import restoring exactly 2 pages confirms full recovery. A gbrain import ~/brain/ under Jay's session HOME reported
"2 pages imported / 2 chunks" — it OVERWROTE those DB pages with the stale 2026-06-09-era content
and embedded it. So a "pages imported" result from the wrong tree is actively harmful, not benign.
RECOVERY: immediately re-import canonical (HOME=/home/itadmin gbrain import /home/itadmin/brain/ --no-embed — it re-imports the same N pages as "changed") + gbrain embed --stale, then VERIFY the
DB matches canonical: gbrain get index | grep -c 'session-2026' == grep -c 'session-2026' /home/itadmin/brain/index.md, and spot-diff a shared page body (diff <(gbrain get people/joe-castelino | grep -A100 '^# ') <(grep -A100 '^# ' /home/itadmin/brain/people/joe-castelino.md)).
Stale-ONLY slugs (caliber-ops, tekion-dms, etc.) will linger with stale content in the DB — they've
been there historically and are low harm, but don't re-feed them.
THE DREAM-CYCLE CRON HAS THE SAME TRAP (hit 2026-07-17): the nightly dream cron prompt says
gbrain dream --dir ~/brain — under Jay's session HOME its sync phase full-reimports the stale
7-file ~/brain tree ("Found 7 markdown files... 2 pages imported" = index + joe-castelino
clobbered; a "sync anchor object missing, running full reimport" message precedes it). Run the
dream with --dir /home/itadmin/brain (or HOME=/home/itadmin) instead. If already run wrong:
canonical re-import + embed recovers exactly 2 pages; verify index session-ref count DB==disk and
joe-castelino body diff empty. The dream's other phases (extract/consolidate/orphans/purge) operate
on the DB and are unaffected — no need to re-run the whole cycle after recovery.
RECURRED 2026-07-03 — THE BRAIN-SYNC CRON PROMPT ITSELF IS THE TRAP: the cron job's prompt
literally says gbrain import ~/brain/ --no-embed, so every run under Jay's session HOME clobbers
index + people/joe-castelino again ("2 pages imported / 2 chunks" from the 7-file stale tree).
DO NOT run the prompt's commands verbatim. On ANY brain-sync cron run: substitute the canonical
path + cron HOME BEFORE the first import (don't wait to detect the clobber), then proceed with the
happy-path sequence. If you already ran the wrong-tree import, recover per below and verify
(index session-ref count matches disk, joe-castelino body diff clean — both confirmed working
recovery checks on 2026-07-03). Also from that run: orphan scan found the day's session captures
PLUS a skill page (skills/tekion-menu-custom-price-row) unlinked — skill-page orphans clear via
gbrain link skills-index skills/<name> --link-type references (hub→page), same as index→session.
FIX: always import the CANONICAL path explicitly with the cron HOME:

HOME=/home/itadmin gbrain import /home/itadmin/brain/ --no-embed 2>&1 | tail -5
HOME=/home/itadmin gbrain embed --stale 2>&1 | tail -3

Sanity after: gbrain stats Embedded must == Chunks (25 pages / 29 chunks / 29 embedded as of
2026-06-24), both git trees clean. "2 pages imported / N unchanged" with N>0 means you hit the
right tree.

FAILURE #2 — search returns nothing / page not embedded

Cause: page exists in the repo but wasn't synced, OR sync ran without the embedding key so chunks
imported but didn't embed. FIX: run the preamble (so the key is exported), then
HOME=/home/itadmin gbrain sync --repo /home/itadmin/brain --no-pull. Look for
"N pages embedded". Confirm with gbrain stats (Embedded should equal Chunks) and a
gbrain search "...". As of 2026-06-24: 25 pages, 29 chunks, 29 embedded.

CAPTURE A SESSION INTO THE BRAIN (the "store each session so it's findable" piece)

Each session's durable learnings should become a brain page so the nightly refresh embeds them and
gbrain search finds them later. Write a markdown page WITH yaml frontmatter and drop it in
/home/itadmin/brain/projects/<slug>.md:

---
type: session
date: YYYY-MM-DD
title: <short title>
tags: [tekion, parts, ...]
links: [[tekion-parts-replenishment]], [[tekion-apc-tools]]   # wikilink to existing pages, no orphans
---
# <title>
## What we did / ## Key result / ## Files / ## Related

Then capture + sync it live (don't wait for 3 AM):
/home/itadmin/bin/capture-session.sh <slug> < /tmp/page.md (writes, commits, gbrain sync).
Keep pages CONCISE and link them so they aren't orphans. Prefer extending an existing project page
over creating many tiny ones. This is distinct from memory (which is full at ~98% and is for
compact always-injected facts) — the brain holds the fuller searchable record.

SESSION-END AUTO-SYNC (multi-session-per-day capture, added 2026-06-25)

Hermes has NO native session-end hook, so a watcher cron captures FINISHED sessions into the brain
between the daily 3 AM refreshes (Joe's intent: "add a session-end sync... automatically parses and
updates the info at the end of the session"). This is what makes multiple sessions a day get stored.

  • Script: /home/itadmin/bin/session-end-sync.sh. Cron: every 15 min, logs to
    /home/itadmin/.gbrain/session-end-sync.log. State (processed sessions) in
    /home/itadmin/.gbrain/session-sync.state (one session basename per line — never re-processed).
  • Mechanism: scans the jay sessions dir session_2*.json (skips session_cron_* + request_dumps),
    picks ones IDLE >= 20 min (no writes = finished) and not in the state file and >= 8000 bytes,
    extracts the user+assistant transcript, distills it via a ONE-SHOT no-tools LLM call
    (hermes chat -q "$prompt" -Q -t "" --max-turns 1 — the venv hermes at
    /home/itadmin/.hermes/hermes-agent/venv/bin/hermes), then pipes the page into
    capture-session.sh <slug> (commit + gbrain sync + embed — searchable immediately).
  • Gotchas: hermes -Q prepends a session_id: line — the script awk-strips everything before
    the first --- frontmatter line. The distill may return SKIP (nothing durable) — detected on the
    RAW output BEFORE stripping, else it'd look empty and hit the raw fallback. Slug = session-<ts>.
    The live/current session is still being written so the 20-min idle gate safely skips it.
  • First-run seeding: on install, the state file was pre-seeded with all 221 then-existing
    interactive sessions so it would NOT distill the entire backlog — only NEW finished sessions going
    forward. To backfill, remove lines from the state file.
  • Verify: tail /home/itadmin/.gbrain/session-end-sync.log; gbrain stats (Pages grow as
    sessions finish); ls /home/itadmin/brain/projects/session-*.md. Validated 2026-06-25 on the SCT
    4Runner menu session — produced a high-quality distilled page from a 448KB transcript in ~36s.
  • Memory cap raised 2026-06-25 in the two hermes config.yaml files (base + jay profile): the
    memory limit went 18000 to 28000 chars and the user-profile limit 9000 to 12000 (MEMORY.md had
    already exceeded the old 18K). Timestamped .bak copies were kept alongside each config.

SKILL→BRAIN AUTO-BACKFILL NOW WIRED (2026-06-29, Joe directive: "skills should automatically wire into the brain instead of you manually calling it")

Previously backfill-skills-to-brain.py was ONLY run by hand, so new/edited automotive skills
silently accumulated UN-searchable in the brain (found a 13-skill gap on 2026-06-29: 51 SKILL.md
files but only 38 brain/skills/.md pages — incl that day's tol-menu-sales-reports,
tol-alignment-by-advisor-report, tekion-ghost-bin-negative-onhand, tekion-kb-search-scrape, etc.).
NOW session-end-sync.sh (every 15 min) auto-backfills: it runs backfill-skills-to-brain.py
(pure file writes of brain/skills/
.md + brain/memory/*.md + skills-index.md — fast/cheap every pass),
then checks git status --porcelain -- skills/ memory/ skills-index.md; ONLY if something actually
CHANGED does it commit + gbrain import --no-embed + gbrain embed --stale + set captured_any=1
(so build-brain-edges then wires the new pages). Same cheap change-gated pattern as the KB ingest.
Block sits right after the rebuild-skill-index.sh call. VERIFIED 2026-06-29: no-change pass logs
nothing (correct); injecting a stale page → "-> skill backfill: 1 skill/memory page(s) changed",
regenerates it, import+embed, tree clean. So: create/edit a skill → searchable in brain within 15 min,
no manual backfill-skills-to-brain.py call needed. NOTE the backfill only covers the AUTOMOTIVE
skills dir (the 73 generic plugin skills are intentionally excluded as search noise).

SKILL INDEX with descriptions + metadata + usage counts (SELF-MAINTAINING as of 2026-06-25)

⚠️ Folded-description bug — FIXED 2026-08-26, check for regressions

rebuild-skill-index.sh originally parsed description: with an inline-only regex
(^description:\s*(.+)$). Any skill whose frontmatter uses a YAML folded/literal block
scalar

description: >
  Create a NEW opcode from scratch in Tekion's Opcode Management...

— had its description stored in manifest.json as the literal string ">".
39 of 198 skills were affected, including high-traffic ones: tekion-opcode-create,
tekion-autonomous-login, persistent-browser-server, bt-tony-menu-rebuild,
sct-menu-sales-*, tekion-add-job-to-ro-button-disabled. Those skills were effectively
invisible to description-based lookup — a direct contributor to loading the WRONG skills
during the 40-min UC4ALIGN build (Joe: "are you not using the skills?").

Parser now handles >, >-, >+, |, |-, |+ and bare-empty heads by collecting the
following indented block. Verify after EVERY index rebuild:

python3 -c "import json;m=json.load(open('/home/itadmin/.hermes/profiles/jay/skills/manifest.json'));\
b=[s['name'] for s in m['skills'] if s['description'].strip() in ('>','|','>-','|-','')];\
print('broken:',len(b),'of',m['count'])"

Must print broken: 0. Back up usage-stats.json first and confirm the times_used sum
didn't drop — the rebuild merges rather than resets, but a bad edit can wipe counters.

Second known gap (RESOLVED 2026-08-26 — was 95/198 with triggers:, now 131/198 and
125/125 automotive).
triggers: is the strongest matching signal, so when creating or
patching ANY skill, always include a triggers: list of 6-9 natural-language phrases.

⚠️ Legacy singular trigger: key — FIXED 2026-08-26

Separate from the folded-description bug, 18 automotive skills carried a legacy singular
trigger: key
(a comma-separated string) alongside or instead of the plural triggers:
list. rebuild-skill-index.sh only ever reads triggers: — so those phrases were dead
weight, and in several cases the singular key held the RICHER phrasing:

trigger: >                       # ← invisible to the indexer
  tekion computer use, vision browser, autonomous tekion, tekion cu
triggers:                        # ← the only one actually indexed
  - vision guided tekion automation

All 18 merged (singular phrases folded into the plural list, singular key deleted).
Notable gains: tekion-opcode-overrides 4→17, tekion-opcode-default-pricing 4→15,
tekion-ghost-bin-negative-onhand 6→15, bc-menu-sales-reports 8→14.

PITFALL that bit me during the merge: some files write trigger: > as a FOLDED BLOCK,
not a single line. A regex that deletes only the ^trigger: line orphans the indented body
into the description block, silently corrupting it. Deletion must be block-aware
remove the key line PLUS all following indented non-blank lines. Always assert
description is byte-identical before/after and write only if it passes.

Standing invariants — assert all four after ANY skill-index work

/home/itadmin/.hermes/hermes-agent/venv/bin/python3.11 -c "
import json,yaml,glob,re
root='/home/itadmin/.hermes/profiles/jay/skills'
m=json.load(open(root+'/manifest.json'))
print('broken desc :',len([s for s in m['skills'] if s['description'].strip() in ('>','|','>-','|-','')]))
a=[s for s in m['skills'] if s['category']=='automotive']
print('auto no-trig:',len([s for s in a if not s['triggers']]),'of',len(a))
p=[]
for f in glob.glob(root+'/**/SKILL.md',recursive=True):
    mm=re.match(r'^---\s*\n(.*?)\n---',open(f).read(),re.S)
    if not mm: p.append(f); continue
    try: d=yaml.safe_load(mm.group(1))
    except Exception: p.append(f); continue
    if not isinstance(d,dict) or 'trigger' in d or str(d.get('description','')).strip() in ('>','|',''): p.append(f)
print('yaml probs  :',len(p))
"

Expect broken desc: 0, auto no-trig: 0, yaml probs: 0, and the usage-stats.json
times_used sum must not drop. Back up usage-stats.json to /tmp before rebuilding.

State as of 2026-08-26: 198 skills, 0 broken descriptions, 125/125 automotive skills
have triggers, 131/198 overall (the 67 without are generic bundled skills — creative,
mlops, github — not AMG work).

Files under /home/itadmin/.hermes/profiles/jay/skills/:

  • manifest.json — enriched: per skill {name, skill_name, path, description, triggers,
    category, size_bytes, modified}. Regenerate with /home/itadmin/bin/rebuild-skill-index.sh
    (parses each SKILL.md frontmatter; preserves usage counters; adds new skills at 0; drops deleted).
  • usage-stats.json{skills:{<name>:{times_used,last_used}}}. Survives rebuilds.
  • usage-log.ndjson — append-only audit (one {ts,skill} JSON line per use).
  • RESOLVER.md — curated trigger→skill table (hand-maintained).

AUTO-MAINTENANCE (no manual steps needed — wired into session-end-sync, runs every 15 min):

  1. Manifest auto-rebuild: session-end-sync.sh calls rebuild-skill-index.sh at the end of
    every run, so descriptions/sizes/modified-dates/new+deleted skills stay current automatically.
  2. Auto usage logging: for every FINISHED session it captures, session-end-sync.sh runs
    /home/itadmin/bin/log-skill-uses-from-session.py <session.json> which scans that session's
    tool_calls for skill_view/skill_manage and bumps times_used + stamps last_used. KEY
    MAPPING: tool-call args carry the BARE skill name (tekion-foo) but usage-stats keys are the
    CATEGORY PATH (automotive/tekion-foo) — the helper maps bare→path via the manifest's
    skill_namename. Counts +1 per skill PER SESSION (distinct use), not per call, so reloading a
    skill 5× in one session = 1 genuine use. This REPLACES manually remembering log-skill-use.sh.
    Verified 2026-06-25: correctly credited automotive/tekion-quotes-menu-price-diagnosis (0→1).
  • The manual /home/itadmin/bin/log-skill-use.sh <category/skill-name> still works for one-off
    same-session crediting, but is now mostly redundant.
    See top movers: python3 -c "import json;d=json.load(open('/home/itadmin/.hermes/profiles/jay/skills/usage-stats.json'));print(sorted([(v['times_used'],k) for k,v in d['skills'].items() if v['times_used']>0],reverse=True))"

RUN-LOG ARCHIVE (condensed 2026-08-18 — was 99 near-duplicate sections / ~42KB)

Every past "CLEAN NO-OP RE-CONFIRMED", "ORPHAN REPAIR RUN", and "FULL DREAM CYCLE" per-run entry now
lives in references/run-log-archive.md. They all said the same three things, so read the archive only
if you need a specific historical count:

  1. Clean no-op run = disk .md count == import scan total (0 imported / N skipped, 0 errors),
    embed --stale = "0 chunks", stats Embedded==Chunks, orphans 0, git status clean. Nothing to
    do — respond [SILENT].
    BUT NEVER STOP ON THE FIRST CLEAN PASS — ALWAYS RE-IMPORT ONCE TO CONFIRM (learned 2026-08-26).
    A first import can report a PERFECT no-op (0 imported / 1293 skipped / 0 errors, embed --stale
    0 stale, orphans 0/1299, git status clean) and still be wrong: the every-15-min session-end-sync
    commits new projects/session-* captures mid-run, so the "clean" reading is just a snapshot taken
    before they landed. On 2026-08-26 that exact clean first pass was followed by a plain re-import that
    found 1 page imported / 1 chunk, which then surfaced 1 orphan — and clearing it cascaded into a
    SECOND latecomer (session-20260826_064650_59276f) on the next pass. Two full link→index.md→commit→
    reimport→embed rounds were needed before orphans finally hit 0/1301 with git status clean.
    So the exit condition is never "the first pass looked clean"; it is two consecutive passes agreeing:
    re-run gbrain import --no-embed + embed --stale + orphans and only declare [SILENT] when that
    CONFIRMING pass is also 0-imported / 0-stale / 0-orphans / clean tree. Cost is ~5s; the failure mode
    it prevents is silently leaving a fresh session page unembedded and unlinked until the 3 AM refresh.
    TIP (2026-08-26 11:00 cron): put a sleep 15-20 at the START of the confirming pass. The pass costs
    only ~5s, so back-to-back imports can both finish inside the same session-end-sync write window and
    both miss the same in-flight capture; the short wait lets it land and be caught in THIS run instead of
    surfacing as an orphan two rounds later. Confirmed: clean pass 1 (0/1308) → sleep 20 → pass 2 found
    1 page/2 chunks + 1 orphan, then the usual 2b mixed session+skill orphan round, settling at 0/1317.
  2. Orphan repair run = import picks up 1-3 fresh same-day projects/session-* captures; clear each
    with hub→page gbrain link index projects/session-<ts> --link-type references, append to index.md
    ## Sessions, git add -A / git commit -m "brain sync" (separate calls), re-import + embed --stale,
    then re-check orphans for latecomers.
    2b. ORPHANS CAN APPEAR ON A PASS THAT IMPORTED NOTHING — always run orphans every pass, never
    gate it on the import counts (learned 2026-08-26 10:00 cron).
    Sequence seen: pass 1 imported 1
    page/1 chunk → 1 orphan (session-20260826_085328) → linked + index.md + commit. Pass 2's re-import
    reported 0 pages imported / 0 chunks / 0 stale — which reads like "done" — yet orphans then
    returned 2: a latecomer session (session-20260826_100035_ebab3a) AND a SKILL page
    (skills/tekion-department-opcode-buildout, written earlier by the auto-backfill and already in the
    DB, just never edge-linked). So "0 imported" only means no page CONTENT changed; it says nothing about
    missing edges. Clear mixed orphan sets per-hub in one pass: gbrain link index projects/session-<ts> --link-type references for the session, gbrain link skills-index skills/<name> --link-type references
    for the skill page (skill pages do NOT go in index.md's ## Sessions list — only sessions do).
    Then commit + re-import (index.md re-chunked: "1 page imported / 25 chunks") + embed --stale
    (1 chunk) → orphans 0/1314, then one CONFIRMING pass (0 imported / 0 stale / 0 orphans / clean tree)
    before [SILENT]. Total: 3 rounds, ~2 min.
  3. Do NOT append a new per-run section to this SKILL. SKILL.md has a hard 100,000-char limit and
    these logs blew past it (skill_manage patch rejected 2026-08-18 at 101,895 chars). Only record a run
    here if it revealed something GENUINELY NEW (a new failure mode, a flag change, a different fix).
    Routine runs get nothing.

NORMAL SYNC RUN, 2 ORPHANS CLEARED (2026-08-18 09:30 cron): happy-path import found 2 pages

imported (2 new session captures from the 15-min sync that hadn't been linked yet:
session-20260818_090210_d75ff1 "Nightly brain sync cron verified canonical path",
session-20260818_090931_46248770 "Blackstone Toyota Random Requests Channel Context Captured"),
embed --stale embedded those 2 chunks. orphans found exactly those 2 (both [projects], no [kb]
latecomers this run). Linked both gbrain link index projects/session-<ts> --link-type references
-> ok, added both to index.md Sessions list (title pulled from gbrain get <slug> frontmatter),
committed ("brain sync"), re-imported (1 page/22 chunks — index.md itself re-chunked since it's
large) + re-embedded (2 chunks — the rest were already fresh from the same pass). Final verify:
orphans 0/1143, git clean, stats 1143 pages/2219 chunks/2219 embedded/1992 links. Confirms the
one-orphan-round happy path (no self-referential second round needed this time — this session's own
capture apparently lands in a LATER 15-min sync pass, not this one).

DIAGNOSTIC: Embedded < Chunks with embed --stale = 0 is usually a TRANSIENT, not a failure (2026-07-23)

Mid-sync gbrain stats can show Embedded (e.g. 1627) < Chunks (1637) while gbrain embed --stale
reports "0 stale found" — looks like the Embedded==Chunks invariant is broken with no way to fix it.
Do NOT remediate yet. Triage: (1) gbrain embed --all --dry-run — if it just counts all chunks
normally and gbrain doctor shows embed_staleness: No stale chunks + embed 35/35, nothing is
actually broken; (2) finish the normal happy-path (link orphans, edit index.md, commit, final
re-import + embed --stale) — the gap cleared to 1637/1637 after that final pass on 2026-07-23.
Only investigate further if the gap PERSISTS after the final re-import+embed.
ALSO: gbrain doctor's "Overall health score" can read 5/100 from unrelated FAILs
(cycle_freshness "never completed a full cycle", resolver_health, content_sanity warn-count) while
the sync-relevant checks are all green (embed 35/35, links 25/25, orphans 15/15). For brain-sync
verification, trust the specific checks (embed_staleness, stats Embedded==Chunks, orphans 0, git
clean), not the headline score — the dream-cycle/resolver failures belong to the 3AM refresh, not sync.

DIAGNOSTIC: gbrain list CAPS session display at ~42 — NOT an import-skip / missing-page bug (2026-06-26)

FALSE-ALARM trap during brain-sync: comparing gbrain list -n 1000 | grep session | sort -u | wc -l
(showed 42) against ls brain/projects/session-*.md (53 files) looks like 11 committed pages were
import-skipped/missing. THEY ARE NOT MISSING. In v0.42.37 gbrain list silently caps the session
rows it prints (~42) regardless of --limit/-n or --include-deleted. Proof the pages are fine:
gbrain get projects/session-<ts> returns full frontmatter+body, AND gbrain query "<topic>"
returns them at high relevance (0.84–0.93), AND gbrain backlinks index lists all 53. So do NOT
trust gbrain list for a complete page census or as the import-skip detector. Use the AUTHORITATIVE
checks instead: (a) gbrain stats Embedded==Chunks (health), (b) gbrain backlinks index | grep session | sort -u | wc -l == ls brain/projects/session-*.md | wc -l (link completeness), (c) spot
gbrain query for a known topic. Also: touch-ing a .md no longer forces re-import in v0.42.37
(importer is content-hash based, not mtime) — the touch→import FIX documented above for FAILURE
"IMPORT can SKIP a committed page" may be ineffective; verify the page is genuinely absent via
gbrain get <slug> (errors if truly missing) BEFORE attempting any re-import remediation.

DETECTOR GOTCHA — a per-file gbrain get LOOP can give a transient FALSE-NEGATIVE (2026-06-28):
When hunting import-skipped pages, do NOT trust a for f in session-*.md; do gbrain get $slug; done
loop as the authority — on 2026-06-28 (v0.42.21) that loop reported "0 missing" on one pass while a
STANDALONE gbrain get projects/session-<ts> for the same slug consistently returned page_not_found,
and the disk-vs-backlinks comm -23 consistently flagged it. The AUTHORITATIVE missing-page detector is
the link-completeness diff: comm -23 <(ls brain/projects/session-*.md|sed 's#.*/##;s#\.md$##'|sort -u) <(gbrain backlinks index|grep -io 'session-[0-9_a-f]*'|sort -u) — anything it prints, confirm once with
a SINGLE standalone gbrain get <slug> (+ --include-deleted to rule out soft-delete), then fix. Also
note gbrain stats session count can lag the true count (showed 65 while disk=67 and only 1 page was
actually missing) — don't size the gap from stats; size it from the comm diff. The newline-bump fix
below still works on v0.42.21 (1 page imported / 1 chunk embedded, gbrain get then resolved).

WORKING FIX for a content-hash import-skip in v0.42.37 (verified 2026-06-26): when gbrain get projects/session-<ts> returns page_not_found (and --include-deleted confirms it's NOT soft-deleted)
yet the .md is valid + git-committed, the importer has a recorded hash but no page row (state/DB mismatch).
touch does NOT fix it (importer is content-hash, not mtime). The reliable fix is to BUMP THE CONTENT
HASH by appending a harmless extra trailing newline to the file, then re-import:
for each missing file → write back its content + one extra \n, then
gbrain import /home/itadmin/brain/ --no-embed (reports the bumped pages as imported) + gbrain embed --stale,
then gbrain link projects/session-<ts> index --link-type child_of, add to index.md Sessions list,
git add/commit, and FINAL re-import+embed so index.md re-embeds.
Confirmed 2026-06-26: 2 sessions (161728, 162505) were import-skipped this way; newline-bump re-ingested
both (3 pages imported / 3 chunks embedded) and gbrain get then resolved them. NOTE the brain-sync run
is a moving target — the 15-min session-end-sync can commit NEW session pages mid-run, so after your final
your final verify, RE-CHECK comm -23 <(disk sessions) <(backlinks index) once more and link any latecomer (happened
2026-06-26: session-20260626_163841 appeared after the first scan and needed a second link+commit+embed pass).
LATECOMER TRIAGE — gbrain get FIRST to pick the right fix (verified 2026-06-29): when the diff flags
a latecomer, the two cases need DIFFERENT handling and you can't tell which by inspecting the .md.
Run gbrain get projects/session-<ts> immediately: (a) if it RESOLVES (returns frontmatter), the page
was already pulled in by a prior import pass in this same run — just gbrain link ... index --link-type child_of + add to index.md + commit + reimport/embed (NO newline-bump). (b) if it returns
page_not_found (and --include-deleted confirms not soft-deleted), it's a content-hash import-skip —
apply the newline-bump fix THEN link. In the 2026-06-29 run the FIRST latecomer (161548) needed the bump
(committed by session-end-sync AFTER my import, so never ingested) while the SECOND (173215) already
resolved (it rode in as the "1 page imported" of the index.md re-embed pass) and only needed linking.
Doing the cheap gbrain get probe first avoids a needless bump on case (a).
ALWAYS compare import-scan count vs disk count RIGHT AFTER the first import (verified 2026-07-14):
a first import reporting "0 pages imported / N skipped / 0 errors" can look perfectly healthy while
having missed SEVERAL freshly-committed session pages entirely (saw scan total 570 vs 572 .md on disk;
git log showed 3 session commits seconds old). Check: find /home/itadmin/brain -name '*.md' -not -path '*/.git/*' | wc -l vs the import's imported+skipped total. If disk > scanned, just re-run the
plain import — it picked up all 3 as "3 pages imported", no newline-bump needed. Then embed, link the
new orphans hub→page (gbrain link index projects/session-<ts> --link-type references), add to
index.md Sessions list, commit, final re-import+embed. Don't wait for a page_not_found to run this
check — it's the cheapest latecomer detector and gbrain orphans right after the re-import names
exactly the pages needing links.
CASE (b) REFINEMENT — try a PLAIN RE-IMPORT before the newline-bump (verified 2026-07-10): a
page_not_found (not soft-deleted) does NOT always mean a content-hash skip. If session-end-sync
committed the file seconds before your first import, the import scan can MISS the file entirely —
tell-tale: the import's total file count (imported+skipped) is LESS than find brain -name '*.md' | wc -l
(saw 394 scanned vs 395 on disk). A second plain gbrain import /home/itadmin/brain/ --no-embed
picked it up cleanly as "1 page imported" — no bump needed. So triage order for page_not_found:
(1) compare import-scan count vs disk count; (2) re-run plain import + embed; (3) only if it STILL
reports 0 imported and gbrain get still fails, apply the newline-bump.
ALSO re-confirmed 2026-07-10: the disk-vs-index.md wikilink comm diff shows dozens of older
sessions (June 25–Jul 2 era) absent from index.md's Sessions list while gbrain orphans = 0 —
those have edges via build-brain-edges and are FINE. gbrain orphans output is the authoritative
orphan check; only remediate diff entries that are NEW sessions from the current run.

LATECOMERS CAN ARRIVE IN A STREAK — LOOP, don't just re-check once (verified 2026-06-29): a single
brain-sync run had THREE latecomer sessions appear back-to-back (115831, 120206, 120700) as the 15-min
session-end-sync committed them mid-run. Each needed its own link→add-to-index.md→commit→re-import→embed
pass. The clean exit condition is: comm -23 <(disk sessions) <(backlinks index) prints NOTHING, AND
gbrain stats Embedded==Chunks, AND git status clean. Keep looping the link+index+commit+reimport+embed
cycle until that diff is empty on a fresh check. NOTE the happy-path is usually trivial — the FIRST import
reports just "1 page imported / 1 chunk" (the one newest session); the rest of the work is linking it +
re-embedding the edited index.md. ALSO observed 2026-06-29: a git commit may report "nothing to commit"
even after a real index.md edit, because a concurrent session-end-sync pass already git added + committed
your staged change as part of its own flow — verify with git log --oneline -3 (the edit IS captured in a
"brain sync" or "session:" commit); the tree being clean is the success signal, not the commit message.
COROLLARY — the hijacking sync ALSO absorbs your RE-IMPORT, so post-commit "0 pages imported" for a
just-edited index.md is CORRECT, not an import-skip (new 2026-08-31 09:00 cron).
Sequence: edited
index.md (2 new session lines) → git add -A → commit said "nothing to commit, working tree clean"
(session-end-sync had already committed it; git log --oneline -3 showed its session: /
skill backfill: commits on top) → the mandatory step-6 re-import then reported 0 pages imported /
0 chunks / 0 stale
. That LOOKS like index.md's new wikilinks never got re-embedded, and it's exactly
the shape that tempts a needless newline-bump. A hijacked commit does NOT guarantee the DB is current
— it goes BOTH ways, so always run the check, never assume.
If the sync's own gbrain import ran
AFTER your edit landed, the DB already has it (2026-08-31 case). If it ran BEFORE, the sync committed
your edit but never ingested it, and the DB is stale (2026-09-01 09:32 cron: commit hijacked, then
DB grep = 2 vs disk = 3 — a real gap). VERIFY instead of remediating — compare
DB vs disk directly:

HOME=/home/itadmin gbrain get index | grep -c 'session-<YYYYMMDD>_<hh>'   # DB copy
grep -c 'session-<YYYYMMDD>_<hh>' /home/itadmin/brain/index.md            # disk copy

Equal counts (2 == 2 here) = index.md is current in the brain; move on to the orphan re-check. Only if
DB < disk is there a real skip (then plain re-import, and only then the newline-bump). DB<disk branch
walked end-to-end 2026-09-01: the plain re-import reported "3 pages imported / 30 chunks" — index.md
PLUS two latecomer session pages it swept in at the same time — DB grep then matched disk (3==3), and
orphans immediately named those same 2 latecomers. So the fix for a stale index.md and the latecomer
sweep are the SAME plain re-import; expect the imported-page count to exceed 1 and expect fresh orphans
right after (rule 2b). No newline-bump was needed.
Contrast: the very
next round's commit was NOT hijacked, and its re-import behaved textbook — "1 page imported / 27 chunks"
for index.md re-chunking + embed --stale 1 chunk. So both readings are normal; which one you get just
depends on whether a 15-min sync pass overlapped your commit.
Rest of that run was pure documented happy path, nothing new: clean pass 1 (0/1399) → sleep 20 → pass 2
found 2 pages/3 chunks + 2 [projects] orphans → hub→page link index projects/session-<ts> --link-type references ×2 + index.md → commit (hijacked, above) → 1 latecomer orphan → link + index.md + commit
(clean this time) → re-import 1 page/27 chunks + embed 1 → orphans 0/1402 → confirming sleep 20 pass
0 imported / 0 stale / 0 orphans / tree clean → [SILENT]. Final: 1402 pages / 2979 chunks / 2979
embedded / 2493 links. Confirms the sleep-20-before-the-confirming-pass tip and the 2b "run orphans every
pass regardless of import counts" rule both earn their keep.

DIAGNOSTIC: "only N embeddings / gbrain search finds nothing about a topic we worked on"

KEY INSIGHT (2026-06-25): GBrain's embedding count only reflects pages WRITTEN INTO THE BRAIN REPO.
The bulk of Jay's actual knowledge historically lived in TWO stores GBrain never sees: (a) the
memory tool (MEMORY.md, ~22 dense entries) and (b) the 100+ SKILL.md files. So if gbrain stats
shows e.g. 26 pages / 31 embedded while you've done weeks of Tekion work, that is NOT a bug or data
loss — Embedded==Chunks is healthy; the brain simply was never fed the skills+memory. Confirm with
gbrain list -n 100 (count the substantive Tekion pages) vs find /home/itadmin/brain -name '*.md'.
The session-end-sync now feeds NEW sessions in, but the EXISTING skills+memory are a backlog that
must be explicitly backfilled (write/import them as embedded pages) if you want one searchable layer.
This is also the foundation for ingesting Tekion user-manual PDFs — they belong in GBrain as\nembedded pages, NOT in the (size-capped) memory tool.\n\nDECISION MADE + EXECUTED (2026-06-25): Joe chose Option 1 — GBrain as the SINGLE embedded search
layer.
Backfill is DONE: /home/itadmin/bin/backfill-skills-to-brain.py writes 1 page per
Jay-owned automotive skill to brain/skills/<name>.md (38 pages, full SKILL.md body + desc +
triggers, wikilinked to [[skills-index]]), splits MEMORY.md (24 entries) into
brain/memory/jay-memory-0N.md (5 pages, ~5 entries each, [[index]]-linked), and builds the
brain/skills-index.md hub. index.md now links skills-index + the 5 memory pages. Run is
idempotent (overwrites pages). After running the script: git add -A && git commit, then
gbrain import /home/itadmin/brain/ --no-embed + gbrain embed --stale (preamble env first).
Result 2026-06-25: 79 pages / 390 chunks / 390 embedded; skill + memory pages confirmed searchable.
Only the 38 AUTOMOTIVE skills were backfilled (the other 73 are generic plugin skills = search
noise). REMAINING TODO: the TEKION-MANUAL INGESTION PIPELINE (PDF/KB → markdown → capture-session
→ embedded page → linked in index.md) — Joe wants the knowledge graphs to keep growing into THIS
same layer. Re-run backfill-skills-to-brain.py whenever skills/memory change materially.

KB BATCH INGEST DONE (2026-06-25): the 2026-06-24 tekion-kb batch (sitting unsearchable in
/home/itadmin/tekion-kb/) was captured into GBrain via /home/itadmin/bin/ingest-kb-batch-to-brain.py
— writes brain/kb/<slug>.md (frontmatter type:kb, wikilinked to [[kb-index]]) from distilled/.md
(3) + text/
.txt KB articles (11) + transcripts/*.txt Loom walkthroughs (7); slug-dedup collapsed
3 loom_ dupes → 18 files on disk. Built brain/kb-index.md hub, linked from index.md. After
running: git add/commit, gbrain import /home/itadmin/brain/ --no-embed then gbrain embed --stale.
Result: 98 pages / 618 chunks / 618 embedded; KB articles now top-hit relevant queries (e.g. "set
min/max stock quantity"). NOTE: two transcript pages (01-parts-ordering-receiving ~57KB,
03-parts-workflows-pricing ~55KB) exceed gbrain's content-sanity warn threshold — a "consider
splitting" WARNING, not an error; the future manual-ingestion pipeline should chunk long transcripts.
This batch ingest is the STOPGAP; the full PDF/KB→markdown→embedded pipeline remains the separate
project Joe wants built. The "readme -- # Obsidian Vault" top hit on broad searches is NOT a ghost
page — it's the readme text quoted inside the backfilled skills/jay-brain-and-skill-index page; harmless.

AUTO-INGEST OF DROPPED KNOWLEDGE WIRED (2026-06-25, Joe's directive "make sure any knowledge we
drop to you gets ingested into the brain from now on... at the end of every session"):

ingest-kb-batch-to-brain.py is now INCREMENTAL — state file /home/itadmin/.gbrain/kb-ingest.state
(lines <sha1> <relpath>) tracks already-ingested KB files by content-hash, so re-runs only
process NEW or CHANGED files. It reads /home/itadmin/tekion-kb/{distilled/.md, text/.txt,
transcripts/*.txt}, writes brain/kb/.md (frontmatter type:kb, [[kb-index]]), rebuilds the
kb-index hub, and prints JSON {new_ingested,total_kb_pages,new_files}. Standalone: pass --embed
to commit+import+embed inline. session-end-sync.sh now CALLS it every 15-min pass (block added
just before the skill-manifest rebuild): runs the ingest, and IF new_ingested>0 → git add/commit →
gbrain import --no-embedgbrain embed --stale. VERIFIED 2026-06-25 with a self-test file
(borealis-quokka-7741) — dropped into text/, picked up on next sync run ("KB auto-ingest: 1 new
page(s)"), searchable at 0.90, then cleaned up. So: ANY file dropped into /home/itadmin/tekion-kb/
(distilled, text, transcripts) is now auto-embedded into GBrain within 15 min, no manual step.
To remove a stale brain page immediately (don't wait for 3 AM orphan-purge): gbrain delete <slug>
(soft-delete; e.g. gbrain delete kb/<slug>). gbrain sync does NOT purge deleted-on-disk pages.

(superseded) OPEN DECISION raised 2026-06-25: two models for the searchable\nmemory layer — (1) GBrain as the SINGLE embedded search layer: backfill all ~38 automotive skills +\nmemory entries into GBrain pages and route manuals there too (one query searches everything; Jay's\nrecommendation; cheap duplication) vs (2) keep layers separate (skills stay in skill system, GBrain\nholds only sessions + manuals + concepts). TWO TODOs flow from whichever is chosen: (a) BACKFILL the\nexisting skills+memory into GBrain as embedded pages; (b) build the TEKION-MANUAL INGESTION PIPELINE\n(PDF/KB article → structured markdown page(s) → capture-session.sh → embedded GBrain pages → linked\nin index.md). Do NOT start the big backfill until Joe picks the model.

Pitfalls

  • UPGRADE_AVAILABLE banner pollutes STDOUT on every command (seen 2026-08-21, installed 0.42.21.0,
    offered 0.46.25.0; the OFFERED version keeps advancing — 0.47.3.0 as of 2026-08-28 — so don't read a
    specific version string here as canonical or as evidence the upgrade was taken; installed is still
    0.42.21.0 and that is fine).
    Two lines (UPGRADE_AVAILABLE 0.42.21.0 0.46.25.0 + gbrain 0.42... available. Run: gbrain self-upgrade) are prepended to gbrain link / get / orphans output, so anything that
    parses the JSON ({"status":"ok"}) or the frontmatter must strip them: pipe through
    | grep -v -i upgrade (or head/tail past them). Do NOT run gbrain self-upgrade from inside a
    brain-sync cron
    — a mid-run version jump would change import/chunk behavior with no chance to verify;
    raise it to Joe as a separate maintenance task instead. NOTE the skill body cites v0.42.37 behaviors
    (content-hash import, list 42-row cap) while the box actually runs 0.42.21 — those notes still held true.
  • Run gbrain with HOME=/home/itadmin (the cron HOME). Jay's session HOME differs; both configs
    point at the same DB, but be consistent.
  • bun is at /home/itadmin/.hermes/node/bin/bun — if gbrain errors, this PATH is almost always why.
  • Export the embedding key (source /home/itadmin/.gbrain/.env) before sync or embedding is skipped silently.
  • Don't touch /home/itadmin/.hermes/profiles/don-ready/home/.gbrain — that's Don's brain.
  • && git chains trip the terminal backgrounding guard — run git add / git commit / gbrain sync
    as separate terminal calls, or via capture-session.sh.
  • The brain repo is gitignored in places + shared — Jay pages live among other agents'; that's
    expected. A future task may split out a Jay-only DB (queued, low priority).
  • Memory tool is AT ITS HARD CAP — put the fuller record in the brain, keep memory for compact facts.

MEMORY TOOL AT CAP — budget FIRST, never shrink by trial-and-error (learned the hard way 2026-09-02)

The real limits are 56,000 chars memory / 12,000 user-profile (NOT the 28000/12000 the
session-end-sync section above cites — that config raise was superseded). Memory sits at ~99%
essentially permanently, so every memory(action='add') of a substantial entry WILL be rejected:
Memory at 55,872/56,000 chars. Adding this entry (1686 chars) would exceed the limit.

WHAT I DID WRONG (cost ~25 wasted tool calls in one turn): I tried add, got rejected, switched
to replace on an overlapping entry, and then shaved 10-40 chars off the new text and retried —
twenty-plus times in a row (57,172 → 56,623 → 56,547 → ... → 56,001 → success). Each retry is a
full tool round-trip and the error only ever tells you the projected total, so this converges
agonizingly slowly. Joe was waiting.

CORRECT PROCEDURE:

  1. The rejection error prints current_entries (the FULL list) plus usage. Read it once — that's
    your whole working set, no extra call needed.
  2. Compute the budget before writing anything. The replace action's arithmetic is
    new_total = current_total - len(old_entry) + len(new_entry). So:
    max_new_len = 56000 - current_total + len(old_entry_being_replaced).
    Find the entry you're superseding in current_entries, get its length, do the subtraction, and
    write to that number on the first attempt.
  3. Prefer CONSOLIDATING REPLACE over ADD. New knowledge usually supersedes or extends an existing
    entry. Merging the old + new into one replace often nets negative growth. The 2026-09-02 case:
    a new Tekion DEALER_QUOTA finding merged into the existing OVERALL_QUOTA entry — same topic,
    one entry, three buckets documented instead of two overlapping entries.
  4. old_text only needs a short unique substring of the target entry — but note it matches the
    entry containing it, so a longer prefix does NOT reduce the replacement size. Lengthening
    old_text to try to "consume" more budget does nothing; only the entry's full length counts.
  5. If even a consolidated replace won't fit, evict: action='remove' an entry whose content is
    already fully captured in a skill (memory should not duplicate skill bodies), then add.
  6. Escape hatch — don't fight the cap at all. If the knowledge is >~1,200 chars or narrative,
    it belongs in a SKILL or a brain page, not memory. Memory is for compact always-injected facts
    that change behavior. A long incident writeup goes to a skill + gets auto-embedded into GBrain
    within 15 min (see the auto-ingest sections above). Reserve memory edits for the one-or-two-line
    distillation that must be in-context every turn.

Verify after: the success response echoes usage (e.g. 99% — 55,989/56,000) and the full
entry_count. Confirm the entry you intended to supersede is GONE from the returned list — a
replace that silently matched a different entry is the failure mode to watch for.