Three "reflects" — which is which?
The word reflect shows up three times in this stack, naming three different things. Reading the code without keeping these straight is the single biggest source of confusion. Anchor on this table before anything else:
Mental model: reflect-kb is the data layer (nouns), the plugin is the orchestrator (verbs that fire on Claude lifecycle events), the /reflect skill is the LLM-side worker the orchestrator hires to produce content. They communicate through the filesystem (~/.learnings/, ~/.reflect/) and one shell-out boundary (the reflect CLI). No shared Python imports, no embedded copies, no shared utilities. A Codex/Copilot adapter could replace the plugin entirely without touching reflect-kb.
Pipeline at a glance
The orange box is today's load-bearing fix — entity_store.py's sidecar parser was crashing on 18 of 756 sidecars under ~/.learnings/. The orange plane below is brand-new: a structured error sink + statusline badge so future failures are impossible to miss.
Walkthrough — five steps end-to-end
The session writes auto-memory files when it learns something durable — corrections, project facts, references. Each lives as a tiny markdown file with YAML frontmatter under the project's memory directory. Nothing in reflect-kb or the plugin runs here; this is the Claude Code session itself. The file just sits on disk waiting for the next context-compaction event.
show example
~/.claude/projects/-Users-stevengonsalvez--agents-in-a-box-.../memory/ feedback_governance_in_code_not_prompts.md --- name: Governance must be deterministic code, not LLM discipline description: Fleet governance belongs in hooks/scripts, not standing-orders prose type: feedback --- When a fleet rule must be enforced, encode it as a deterministic hook (Stop/UserPromptSubmit/PreCompact) -- not as a STANDING_ORDERS rule. **Why:** Stevie's hard line during Pass 6 / loop-design.
When the session is about to compact, Claude Code fires PreCompact. The plugin's hook appends a one-line JSON record to ~/.reflect/pending_reflections.jsonl — the transcript path + session id + timestamp. This was dead from 2026-05-09 to today: the v3 schema migration replaced ~/.reflect/reflect-state.yaml with a deprecation stub, and is_auto_reflect_enabled() returned False for everything because the key it read no longer existed. Today's fix: env-var-driven, ON by default, REFLECT_AUTO_REFLECT=0 to disable.
show source
# hooks/precompact_reflect.py — fix landed in commit 9d9eb62 def is_auto_reflect_enabled() -> bool: """ Auto-reflect is ON by default when the plugin is installed. Override with REFLECT_AUTO_REFLECT=0 to disable. Legacy reflect-state.yaml is no longer consulted (deprecated in v3 migration on 2026-05-09; would always return False because the migration stub does not carry the auto_reflect key). """ val = os.environ.get('REFLECT_AUTO_REFLECT', '').strip().lower() if val in ('0', 'false', 'no', 'off'): return False return True
On SessionStart, the drain script reads the queue with a PID lockfile (single-flight) and a daily cost cap. For each entry it spawns a child Claude session via claude -p "/reflect <transcript>". The child loads the markdown /reflect skill (not the CLI — a different beast entirely; see Gotchas) and writes .md + .entities.yaml documents to ~/.learnings/documents/. New today: every failure path (stale transcript, poison after retries, no_output, max_turns exhausted, reindex non-zero) writes one deduped record to ~/.reflect/errors.json via the shell helper below.
show source
# hooks/reflect-drain-bg.sh — emit_error helper (commit 8c0bc77) emit_error() { # emit_error <severity> <kind> <message> [transcript_path] local severity="$1" kind="$2" message="$3" transcript="${4:-}" python3 -m reflect_kb.errors append \ --severity "$severity" --source drain --kind "$kind" \ --message "$message" \ --context "$(printf '{"transcript_path":"%s"}' "$transcript")" \ >/dev/null 2>&1 || true } # wired to: drain_stale, drain_poison, drain_no_output, # drain_max_turns_exhausted, reindex_fail
This is the trust boundary for data. reflect reindex walks ~/.learnings/documents/**/*.entities.yaml and converts each to nano-graphrag's extraction format. The bug: a YAML key with no value (e.g. relationships: with nothing after) parses as Python None, not []. So for r in data.get("relationships", []): crashed with TypeError: 'NoneType' object is not iterable. Sweep over 756 sidecars: 18 broken before, 0 after. Same code path also handled the rarer KeyError: 'description' when an entity dict omitted the field.
show source
# src/reflect_kb/cli/entity_store.py — fix landed in commit 3489d3c @classmethod def from_yaml(cls, yaml_str: str) -> "DocumentEntities": data = yaml.safe_load(yaml_str) or {} # NEW: tolerate "" entities = [ Entity( name=e.get("name", ""), type=e.get("type", "concept"), description=e.get("description", ""), # NEW: was e["description"] ) for e in (data.get("entities") or []) # NEW: was [], crashed on None ] relationships = [ Relationship( source=r.get("source", ""), target=r.get("target", ""), type=r.get("type", "relates_to"), description=r.get("description", ""), strength=r.get("strength", 5), ) for r in (data.get("relationships") or []) # NEW: same guard ] return cls(...)
Two new surfaces for the same data. errors.py is a small stdlib-only module — file-locked atomic writes to ~/.reflect/errors.json, 24h dedupe by sha1(source|kind|message), 200-record cap, CLI subcommands append / count / ack. The statusline reads python -m reflect_kb.errors count (cached 10s) and renders a red ⚠N segment on line 1 between git and beads when N > 0. Click-clear via ~/.claude/skills/reflect/scripts/ack_errors.sh. For everyone reading: failures are now impossible to miss — they reach the badge within 10 seconds.
show source
# statusline.sh — render block (commit bb50b1b) if (( CACHE_AGE < CACHE_TTL )); then REFLECT_ERR_COUNT=$(_cache_get "reflect_err_count") else REFLECT_ERR_COUNT=$(timeout 0.4 python3 -m reflect_kb.errors count 2>/dev/null || echo 0) _cache_set "reflect_err_count" "${REFLECT_ERR_COUNT:-0}" fi if [[ "$REFLECT_ERR_COUNT" =~ ^[0-9]+$ ]] && (( REFLECT_ERR_COUNT > 0 )); then L1+=$(_seg "$C_RED" "$C_WHITE" "⚠${REFLECT_ERR_COUNT}" "$prev") prev="$C_RED" fi