From 5399c06a5def1ee37ff19c47be99d52223adf5f4 Mon Sep 17 00:00:00 2001 From: galimba Date: Tue, 7 Jul 2026 16:56:29 +0200 Subject: [PATCH] feat(vault): add MEMORY.md root pointer index and memory-refresh command MEMORY.md is a thin (<200 lines), agent-editable entry-point file: categorized pointers to the core files, rules, latest lint report, and recently active wiki pages. New module lib-memory.sh implements vault-tools.sh memory-refresh, which regenerates it deterministically (idempotent; recently-active section derived from git history with quotePath disabled for non-ASCII names). Doctor validates presence (warning-level, so older instances do not hard-fail) and the sub-200 line budget. Context loading order in CLAUDE.md, AGENTS.md, and CODEX.md now reads MEMORY.md as step 2. Adds tests/test-memory-refresh.sh. Design per issue discussion: agent-editable operational state with a deterministic refresh command, not an HR-012-protected file. Closes #11 Co-Authored-By: Claude Fable 5 --- .vault/scripts/lib-manage.sh | 15 ++ .vault/scripts/lib-memory.sh | 128 ++++++++++++ .vault/scripts/tests/test-memory-refresh.sh | 204 ++++++++++++++++++++ .vault/scripts/vault-tools.sh | 4 + AGENTS.md | 9 +- CHANGELOG.md | 10 + CLAUDE.md | 10 +- CODEX.md | 5 +- MEMORY.md | 32 +++ 9 files changed, 407 insertions(+), 10 deletions(-) create mode 100644 .vault/scripts/lib-memory.sh create mode 100755 .vault/scripts/tests/test-memory-refresh.sh create mode 100644 MEMORY.md diff --git a/.vault/scripts/lib-manage.sh b/.vault/scripts/lib-manage.sh index 46d1aaf..cf4f769 100644 --- a/.vault/scripts/lib-manage.sh +++ b/.vault/scripts/lib-manage.sh @@ -221,6 +221,21 @@ cmd_doctor() { fi done + # MEMORY.md is warning-level only: instances upgrading from template + # versions that predate it should not hard-fail doctor. + local memory_md="${VAULT_ROOT}/MEMORY.md" + if [[ ! -f "$memory_md" ]]; then + warning "MEMORY.md — missing. Generate it: vault-tools.sh memory-refresh" + else + local memory_md_lines + memory_md_lines=$(wc -l < "$memory_md" | tr -d ' ') + if [[ $memory_md_lines -ge 200 ]]; then + warning "MEMORY.md has ${memory_md_lines} lines (must stay under 200). Regenerate: vault-tools.sh memory-refresh" + else + ok "MEMORY.md (${memory_md_lines} lines)" + fi + fi + subheader "Initialization State" if [[ ! -f "${VAULT_ROOT}/.vault/.initialized" ]]; then warning "Vault not initialized. Run: bash .vault/scripts/init.sh" diff --git a/.vault/scripts/lib-memory.sh b/.vault/scripts/lib-memory.sh new file mode 100644 index 0000000..a71bc5e --- /dev/null +++ b/.vault/scripts/lib-memory.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# ============================================================================== +# LIB-MEMORY — MEMORY.md pointer index tooling for vault-tools +# ============================================================================== +# +# Contains commands for maintaining the root MEMORY.md entry-point file: +# cmd_memory_refresh() — Regenerate MEMORY.md deterministically from +# the current vault state +# +# MEMORY.md is a thin (<200 line) pointer index agents load right after +# CLAUDE.md. It holds pointers only — the full page catalog lives in +# wiki/index.md. It is agent-editable operational state (NOT protected by +# HR-012) and safe to regenerate at any time: two consecutive runs yield +# identical content except the "Refreshed:" date line. +# +# This file is sourced by vault-tools.sh and depends on functions and +# variables from lib-utils.sh and the entry point configuration. +# +# ============================================================================== + +# This file is sourced by vault-tools.sh — do not execute directly +[[ "${BASH_SOURCE[0]}" == "${0}" ]] && { echo "Source this file, don't execute it directly."; exit 1; } + +# ============================================================================== +# COMMAND: memory-refresh +# ============================================================================== +# Regenerate MEMORY.md from the current vault state. +# +# Usage: vault-tools.sh memory-refresh +# +# Sections: +# Core — wiki/index.md, wiki/log.md, memory/status.md +# Rules — hard-rules.md, soft-rules.md, tags.md +# Latest Lint Report — newest memory/notes/lint-report-*.md, or "none yet" +# Recently Active Pages — up to 10 wiki pages by git last-touched date +# (index.md/log.md excluded; "none yet" when empty; +# a note when the vault is not a git repository) + +cmd_memory_refresh() { + header "Refreshing MEMORY.md" + + local memory_md="${VAULT_ROOT}/MEMORY.md" + local today + today=$(date +%Y-%m-%d) + + # --- Latest lint report (gitignored; newest by date-stamped filename) --- + local lint_line + local latest_report + # `|| true`: find exits 1 when memory/notes/ does not exist, which would + # kill the script through pipefail inside the command substitution. + latest_report=$(find "${MEMORY_DIR}/notes" -maxdepth 1 ! -type l \ + -name "lint-report-*.md" -type f 2>/dev/null | sort | tail -n 1 || true) + if [[ -n "$latest_report" ]]; then + lint_line="- [[${latest_report#"${VAULT_ROOT}"/}]]" + else + lint_line="- none yet — run \`bash .vault/scripts/vault-tools.sh lint --report\`" + fi + + # --- Recently active wiki pages (git last-touched date, newest first) --- + # One git-log walk over wiki/: the first time a file appears is its most + # recent touch. Core pointers (index.md, log.md) and files no longer on + # disk are skipped. Capped at 10 entries for a stable, thin file. + local active_lines="" + if git -C "${VAULT_ROOT}" rev-parse --git-dir >/dev/null 2>&1; then + local line current_date="" count=0 + declare -A seen=() + while IFS= read -r line; do + [[ $count -ge 10 ]] && break + if [[ "$line" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + current_date="$line" + continue + fi + [[ "$line" == wiki/*.md ]] || continue + [[ "$line" == "wiki/index.md" || "$line" == "wiki/log.md" ]] && continue + [[ -n "${seen[$line]+x}" ]] && continue + [[ -f "${VAULT_ROOT}/${line}" ]] || continue + seen["$line"]=1 + active_lines+="- [[${line}]] (${current_date})"$'\n' + count=$((count + 1)) + done < <(git -C "${VAULT_ROOT}" -c core.quotePath=false log \ + --format='%ad' --date=short --name-only -- wiki/ 2>/dev/null) + [[ -z "$active_lines" ]] && active_lines="- none yet"$'\n' + else + active_lines="- not available — vault is not a git repository"$'\n' + fi + + # --- Write the file --- + { + echo "# MEMORY.md — Vault Entry Points" + echo "" + echo "Thin pointer index for agents: load this right after \`CLAUDE.md\` to find" + echo "the vault's key files in one read. Pointers only — the full page catalog" + echo "lives in [[wiki/index.md]]." + echo "" + echo "Refreshed: ${today}" + echo "" + echo "## Core" + echo "" + echo "- [[wiki/index.md]] — master catalog of all wiki pages" + echo "- [[wiki/log.md]] — append-only chronological record of all operations" + echo "- [[memory/status.md]] — current vault health and operational state" + echo "" + echo "## Rules" + echo "" + echo "- [[.vault/rules/hard-rules.md]] — enforced constraints (violations block commits)" + echo "- [[.vault/rules/soft-rules.md]] — configurable conventions" + echo "- [[.vault/rules/tags.md]] — approved tag taxonomy" + echo "" + echo "## Latest Lint Report" + echo "" + echo "${lint_line}" + echo "" + echo "## Recently Active Pages" + echo "" + printf '%s' "$active_lines" + echo "" + echo "---" + echo "" + echo "Generated by \`bash .vault/scripts/vault-tools.sh memory-refresh\` — safe for" + echo "agents to regenerate at any time." + } > "$memory_md" + + local total_lines + total_lines=$(wc -l < "$memory_md" | tr -d ' ') + ok "MEMORY.md refreshed (${total_lines} lines)" + echo "" + return 0 +} diff --git a/.vault/scripts/tests/test-memory-refresh.sh b/.vault/scripts/tests/test-memory-refresh.sh new file mode 100755 index 0000000..e3f6598 --- /dev/null +++ b/.vault/scripts/tests/test-memory-refresh.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# Test for the memory-refresh command (issue #11). +# +# Builds a disposable vault with a git repo, then asserts: +# 1. memory-refresh creates MEMORY.md under 200 lines with the Core +# pointers (wiki/index.md) and the fixed section headings. +# 2. Recently Active Pages lists a committed wiki page (from git log) +# but never the Core pointers wiki/index.md / wiki/log.md. +# 3. The command is idempotent: a second run differs from the first +# only in the "Refreshed:" date line, or not at all. +# 4. Latest Lint Report switches from "none yet" to the newest +# memory/notes/lint-report-*.md once one exists. +# 5. doctor accepts the generated file (exit 0, no MEMORY.md warning). +# +# Run: bash .vault/scripts/tests/test-memory-refresh.sh +# Exit: 0 on PASS, non-zero on any FAIL. + +set -euo pipefail + +VAULT_ROOT="$(git rev-parse --show-toplevel)" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +TODAY="$(date +%Y-%m-%d)" + +# ------------------------------------------------------------------ +# Build a minimal vault clone in a tempdir with its own git repo so +# the recently-active section has real history to read. +# ------------------------------------------------------------------ +mkdir -p "$TMPDIR"/wiki/{sources,entities,concepts,comparisons} \ + "$TMPDIR"/memory/{decisions,logs,notes} \ + "$TMPDIR/raw" "$TMPDIR/docs" +cp -r "$VAULT_ROOT/.vault" "$TMPDIR/.vault" +cp -r "$VAULT_ROOT/templates" "$TMPDIR/templates" + +touch "$TMPDIR/raw/.gitkeep" + +cat > "$TMPDIR/CLAUDE.md" <<'EOF' +# CLAUDE.md — Test Vault Agent Configuration + +Stub agent configuration for the memory-refresh test vault. +EOF + +cat > "$TMPDIR/AGENTS.md" <<'EOF' +# AGENTS.md — Test Vault Agent Instructions + +Stub agent instructions for the memory-refresh test vault. +EOF + +cat > "$TMPDIR/wiki/index.md" < "$TMPDIR/wiki/log.md" < "$TMPDIR/memory/status.md" < "$TMPDIR/wiki/concepts/concept-memory-test.md" </dev/null \ + || fail "index-rebuild exited non-zero" +git add . +git -c core.hooksPath=/dev/null commit -q -m "seed" \ + || fail "could not create seed commit" + +# ------------------------------------------------------------------ +# Assert 1: memory-refresh creates a thin MEMORY.md with the pointers. +# ------------------------------------------------------------------ +bash .vault/scripts/vault-tools.sh memory-refresh >/dev/null \ + || fail "memory-refresh exited non-zero" +[[ -f MEMORY.md ]] || fail "MEMORY.md was not created" + +lines=$(wc -l < MEMORY.md | tr -d ' ') +[[ $lines -lt 200 ]] || fail "MEMORY.md has ${lines} lines (must be < 200)" + +grep -qF '[[wiki/index.md]]' MEMORY.md \ + || fail "MEMORY.md is missing the wiki/index.md pointer" +for heading in "## Core" "## Rules" "## Latest Lint Report" "## Recently Active Pages"; do + grep -qF "$heading" MEMORY.md || fail "MEMORY.md is missing section: ${heading}" +done +grep -qF 'memory-refresh' MEMORY.md \ + || fail "MEMORY.md footer does not mention the memory-refresh command" + +# ------------------------------------------------------------------ +# Assert 2: the committed concept page shows up as recently active; +# Core pointers are excluded from that section. +# ------------------------------------------------------------------ +active_section=$(awk '/^## Recently Active Pages/,/^---$/' MEMORY.md) +echo "$active_section" | grep -qF 'wiki/concepts/concept-memory-test.md' \ + || fail "committed wiki page missing from Recently Active Pages" +! echo "$active_section" | grep -qF 'wiki/index.md' \ + || fail "wiki/index.md must not appear under Recently Active Pages" +! echo "$active_section" | grep -qF 'wiki/log.md' \ + || fail "wiki/log.md must not appear under Recently Active Pages" + +# ------------------------------------------------------------------ +# Assert 3: idempotent — second run differs only in the date line. +# ------------------------------------------------------------------ +cp MEMORY.md "$TMPDIR/first-run.md" +bash .vault/scripts/vault-tools.sh memory-refresh >/dev/null \ + || fail "second memory-refresh exited non-zero" +if ! diff_out=$(diff "$TMPDIR/first-run.md" MEMORY.md); then + non_date=$(echo "$diff_out" | grep -c '^[<>]' || true) + date_lines=$(echo "$diff_out" | grep -c '^[<>] Refreshed: ' || true) + [[ "$non_date" == "$date_lines" ]] \ + || { echo "$diff_out" >&2; fail "second run changed more than the Refreshed date line"; } +fi + +# ------------------------------------------------------------------ +# Assert 4: newest lint report is picked up on the next refresh. +# ------------------------------------------------------------------ +echo "# Lint Report" > "memory/notes/lint-report-${TODAY}.md" +bash .vault/scripts/vault-tools.sh memory-refresh >/dev/null \ + || fail "memory-refresh exited non-zero after lint report appeared" +grep -qF "[[memory/notes/lint-report-${TODAY}.md]]" MEMORY.md \ + || fail "MEMORY.md does not point at the newest lint report" + +# ------------------------------------------------------------------ +# Assert 5: doctor accepts the generated MEMORY.md (exit 0, no warning +# about MEMORY.md being missing or oversized). +# ------------------------------------------------------------------ +doctor_out=$(bash .vault/scripts/vault-tools.sh doctor 2>&1) \ + || { echo "$doctor_out" >&2; fail "doctor exited non-zero with MEMORY.md present"; } +if echo "$doctor_out" | grep -q 'MEMORY.md.*\(missing\|limit\)'; then + echo "$doctor_out" >&2 + fail "doctor warned about a valid MEMORY.md" +fi + +echo "PASS: memory-refresh generates a valid, idempotent MEMORY.md" diff --git a/.vault/scripts/vault-tools.sh b/.vault/scripts/vault-tools.sh index b7ad924..82d7343 100644 --- a/.vault/scripts/vault-tools.sh +++ b/.vault/scripts/vault-tools.sh @@ -20,6 +20,7 @@ # ./vault-tools.sh content-audit Audit content integrity # ./vault-tools.sh stats Show vault statistics # ./vault-tools.sh init-hooks Install git hooks +# ./vault-tools.sh memory-refresh Regenerate MEMORY.md pointer index # ./vault-tools.sh doctor Full diagnostic check # # EXIT CODES: @@ -88,6 +89,7 @@ done source "${SCRIPT_DIR}/lib-lint.sh" source "${SCRIPT_DIR}/lib-manage.sh" source "${SCRIPT_DIR}/lib-skills.sh" +source "${SCRIPT_DIR}/lib-memory.sh" # ============================================================================== # HELP @@ -115,6 +117,7 @@ cmd_help() { echo " status Show vault status" echo " stats Show detailed vault statistics" echo " index-rebuild Rebuild wiki/index.md" + echo " memory-refresh Regenerate MEMORY.md pointer index" echo " init-hooks Install git hooks" echo " doctor Full diagnostic check" echo " help Show this help" @@ -141,6 +144,7 @@ main() { skill-manifest) cmd_skill_manifest "$@" ;; content-audit) cmd_content_audit "$@" ;; index-rebuild) cmd_index_rebuild "$@" ;; + memory-refresh) cmd_memory_refresh "$@" ;; init-hooks) cmd_init_hooks "$@" ;; doctor) cmd_doctor "$@" ;; help|--help|-h) cmd_help "$@" ;; diff --git a/AGENTS.md b/AGENTS.md index e50eb24..225431d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,10 +26,11 @@ Support directories: `.vault/` (config), `templates/` (page templates), `docs/` ## Context Loading Order 1. Read this file (`AGENTS.md`) or `CLAUDE.md` -2. Read `wiki/index.md` — discover vault contents -3. Read `memory/status.md` — understand current state -4. Read `.vault/rules/hard-rules.md` — understand constraints -5. Load task-specific wiki pages as needed +2. Read `MEMORY.md` — entry-point pointers +3. Read `wiki/index.md` — discover vault contents +4. Read `memory/status.md` — understand current state +5. Read `.vault/rules/hard-rules.md` — understand constraints +6. Load task-specific wiki pages as needed ## Three Operations diff --git a/CHANGELOG.md b/CHANGELOG.md index 635b357..fcf99a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Root `MEMORY.md` — thin, agent-editable pointer index (Core files, rules, + latest lint report, recently active pages) loaded right after `CLAUDE.md`. + Regenerated deterministically by the new `vault-tools.sh memory-refresh` + command (`.vault/scripts/lib-memory.sh`); `doctor` warns when the file is + missing or exceeds 200 lines. Context loading order in `CLAUDE.md`, + `AGENTS.md`, and `CODEX.md` updated to include it. Covered by + `.vault/scripts/tests/test-memory-refresh.sh` (#11). + ## [0.5.0] - 2026-07-07 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 46775b4..b47ba9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,7 @@ docs/ # Vault documentation for humans. How-to guides, onboarding. | `CLAUDE.md` | Root | Agent configuration (this file) | | `AGENTS.md` | Root | Platform-agnostic agent instructions (mirrors this file) | | `CODEX.md` | Root | OpenAI Codex-specific overrides | +| `MEMORY.md` | Root | Generated pointer index of vault entry points | | `index.md` | `wiki/` | Master catalog of all wiki pages | | `log.md` | `wiki/` | Append-only chronological record of all operations | | `status.md` | `memory/` | Current vault health and operational state | @@ -175,10 +176,11 @@ See `.vault/rules/tags.md` for the full taxonomy: 19 prefix categories, 230 appr ### Context Loading Order 1. Read this file (`CLAUDE.md`) — always loaded first -2. Read `wiki/index.md` — understand vault contents -3. Read `memory/status.md` — understand current state -4. Read `.vault/rules/hard-rules.md` — understand constraints -5. Load task-specific wiki pages as needed +2. Read `MEMORY.md` — entry-point pointers +3. Read `wiki/index.md` — understand vault contents +4. Read `memory/status.md` — understand current state +5. Read `.vault/rules/hard-rules.md` — understand constraints +6. Load task-specific wiki pages as needed ### File Naming Conventions diff --git a/CODEX.md b/CODEX.md index fbbbf80..9a4c6fd 100644 --- a/CODEX.md +++ b/CODEX.md @@ -22,8 +22,9 @@ Codex should load context in this order: 1. This file (`CODEX.md`) — Codex-specific overrides 2. `AGENTS.md` — Full operational specification -3. `wiki/index.md` — Vault contents catalog -4. `memory/status.md` — Current vault state +3. `MEMORY.md` — Entry-point pointers +4. `wiki/index.md` — Vault contents catalog +5. `memory/status.md` — Current vault state ### Commit Messages diff --git a/MEMORY.md b/MEMORY.md new file mode 100644 index 0000000..403cad8 --- /dev/null +++ b/MEMORY.md @@ -0,0 +1,32 @@ +# MEMORY.md — Vault Entry Points + +Thin pointer index for agents: load this right after `CLAUDE.md` to find +the vault's key files in one read. Pointers only — the full page catalog +lives in [[wiki/index.md]]. + +Refreshed: 2026-07-07 + +## Core + +- [[wiki/index.md]] — master catalog of all wiki pages +- [[wiki/log.md]] — append-only chronological record of all operations +- [[memory/status.md]] — current vault health and operational state + +## Rules + +- [[.vault/rules/hard-rules.md]] — enforced constraints (violations block commits) +- [[.vault/rules/soft-rules.md]] — configurable conventions +- [[.vault/rules/tags.md]] — approved tag taxonomy + +## Latest Lint Report + +- none yet — run `bash .vault/scripts/vault-tools.sh lint --report` + +## Recently Active Pages + +- none yet + +--- + +Generated by `bash .vault/scripts/vault-tools.sh memory-refresh` — safe for +agents to regenerate at any time.