Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .vault/scripts/lib-manage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,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"
Expand Down
128 changes: 128 additions & 0 deletions .vault/scripts/lib-memory.sh
Original file line number Diff line number Diff line change
@@ -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
}
204 changes: 204 additions & 0 deletions .vault/scripts/tests/test-memory-refresh.sh
Original file line number Diff line number Diff line change
@@ -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" <<EOF
---
title: "Vault Index"
type: index
created: ${TODAY}
updated: ${TODAY}
status: active
tags:
- type/index
owner: agent
confidence: high
---

# Vault Index
EOF

cat > "$TMPDIR/wiki/log.md" <<EOF
---
title: "Test Vault Log"
type: index
created: ${TODAY}
updated: ${TODAY}
status: active
tags:
- type/index
owner: agent
confidence: high
---

# Test Vault Log

## [${TODAY}] ingest | Seeded memory-refresh test vault
EOF

cat > "$TMPDIR/memory/status.md" <<EOF
---
title: "Vault Status"
type: report
created: ${TODAY}
updated: ${TODAY}
status: active
tags:
- type/report
owner: agent
confidence: high
---

# Vault Status

Test vault is operational.
EOF

cat > "$TMPDIR/wiki/concepts/concept-memory-test.md" <<EOF
---
title: "Memory Test Concept"
type: concept
created: ${TODAY}
updated: ${TODAY}
status: draft
sources: []
related:
- "[[wiki/index.md]]"
tags:
- domain/engineering
- type/concept
- lifecycle/active
summary: "A concept page used to exercise the recently-active section."
owner: agent
confidence: high
---

# Memory Test Concept

Links [[wiki/index.md]] and [[wiki/log.md]].
EOF

cd "$TMPDIR" || fail "could not cd into tempdir"
git init -q
git config user.email "test@example.com"
git config user.name "Test Runner"
bash .vault/scripts/vault-tools.sh index-rebuild >/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"
4 changes: 4 additions & 0 deletions .vault/scripts/vault-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
# ./vault-tools.sh consolidate Report stale overlapping pages to merge
# ./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:
Expand Down Expand Up @@ -96,6 +97,7 @@ source "${SCRIPT_DIR}/lib-index.sh"
source "${SCRIPT_DIR}/lib-manage.sh"
source "${SCRIPT_DIR}/lib-skills.sh"
source "${SCRIPT_DIR}/lib-blame.sh"
source "${SCRIPT_DIR}/lib-memory.sh"

# ==============================================================================
# HELP
Expand Down Expand Up @@ -128,6 +130,7 @@ cmd_help() {
echo " index-rebuild Rebuild wiki/index.md (destructive full rewrite)"
echo " index-update Append entries for unregistered wiki pages"
echo " index-split [n] Split index into sub-indexes above n lines (default: 250)"
echo " memory-refresh Regenerate MEMORY.md pointer index"
echo " init-hooks Install git hooks"
echo " doctor Full diagnostic check"
echo " help Show this help"
Expand Down Expand Up @@ -159,6 +162,7 @@ main() {
index-rebuild) cmd_index_rebuild "$@" ;;
index-update) cmd_index_update "$@" ;;
index-split) cmd_index_split "$@" ;;
memory-refresh) cmd_memory_refresh "$@" ;;
init-hooks) cmd_init_hooks "$@" ;;
doctor) cmd_doctor "$@" ;;
help|--help|-h) cmd_help "$@" ;;
Expand Down
Loading
Loading