From b6794df7451bbf785b91fd1d20cf932f8a5e7ed2 Mon Sep 17 00:00:00 2001 From: JTInventory Date: Mon, 29 Jun 2026 00:53:00 +0000 Subject: [PATCH] feat(cognee): add manual memory lookup helper --- bin/fm-memory-lookup.sh | 236 +++++++++++++++++++++++++++++++++ docs/scripts.md | 1 + tests/fm-memory-lookup.test.sh | 99 ++++++++++++++ 3 files changed, 336 insertions(+) create mode 100755 bin/fm-memory-lookup.sh create mode 100755 tests/fm-memory-lookup.test.sh diff --git a/bin/fm-memory-lookup.sh b/bin/fm-memory-lookup.sh new file mode 100755 index 00000000000..5aff823f570 --- /dev/null +++ b/bin/fm-memory-lookup.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# Manual read-only memory lookup for optional pre-dispatch context. +# +# This command is intentionally not wired into dispatch. It only runs when a +# firstmate invokes it by hand. Cognee output is treated as an untrusted hint; +# only local source files that can be opened are eligible for brief attachment. +# Configure a read-only lookup backend with: +# FM_COGNEE_LOOKUP_CMD=/absolute/path/to/read-only-lookup +# The backend is executed as: "$FM_COGNEE_LOOKUP_CMD" "$query" +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}" + +APPEND_BRIEF= +FROM_FILE= +MAX_HINT_LINES=${FM_MEMORY_LOOKUP_MAX_HINT_LINES:-40} +QUERY_PARTS=() + +usage() { + cat >&2 <<'EOF' +usage: fm-memory-lookup.sh [--append-brief path] [--from-file path] -- + +Runs an optional read-only Cognee lookup and prints: + memory hint + verified local source path + warning + +Without FM_COGNEE_LOOKUP_CMD, it exits 0 with a memory-unavailable note so +dispatch can continue without Cognee. +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --append-brief) + [ $# -ge 2 ] || { usage; exit 2; } + APPEND_BRIEF=$2 + shift 2 + ;; + --from-file) + [ $# -ge 2 ] || { usage; exit 2; } + FROM_FILE=$2 + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + --) + shift + while [ $# -gt 0 ]; do + QUERY_PARTS+=("$1") + shift + done + ;; + *) + QUERY_PARTS+=("$1") + shift + ;; + esac +done + +QUERY=${QUERY_PARTS[*]:-} +if [ -z "$QUERY" ] && [ -z "$FROM_FILE" ]; then + usage + exit 2 +fi + +strip_ref_token() { + local value=$1 + value=${value#"${value%%[![:space:]]*}"} + while :; do + case "$value" in + \"*|\'*|\`*|\[*|\(*|"<"*) value=${value#?} ;; + *) break ;; + esac + done + value=${value%%[[:space:],;\)]*} + value=${value%\"} + value=${value%\'} + value=${value%\`} + value=${value%]} + value=${value%>} + value=${value%.} + printf '%s\n' "$value" +} + +extract_source_paths() { + local line rest path seen_file=$1 + : > "$seen_file" + while IFS= read -r line; do + rest= + case "$line" in + *SOURCE_PATH=*) rest=${line#*SOURCE_PATH=} ;; + *SOURCE_PATH:*) rest=${line#*SOURCE_PATH:} ;; + *source_path=*) rest=${line#*source_path=} ;; + *source_path:*) rest=${line#*source_path:} ;; + *"Source path:"*) rest=${line#*"Source path:"} ;; + *"source path:"*) rest=${line#*"source path:"} ;; + esac + [ -n "$rest" ] || continue + path=$(strip_ref_token "$rest") + [ -n "$path" ] || continue + grep -Fx -- "$path" "$seen_file" >/dev/null 2>&1 || printf '%s\n' "$path" >> "$seen_file" + done +} + +canonical_path() { + local path=$1 dir base + case "$path" in + /*) ;; + *) path="$PWD/$path" ;; + esac + dir=$(dirname "$path") + base=$(basename "$path") + [ -d "$dir" ] || return 1 + printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base" +} + +append_brief_section() { + local brief=$1 unavailable=$2 verified_file=$3 warnings_file=$4 + [ -n "$brief" ] || return 0 + mkdir -p "$(dirname "$brief")" + { + printf '\n# Optional memory lookup\n' + printf 'Memory lookup is advisory only. Cognee hints are not proof, source truth, or approval for external action.\n' + if [ -n "$unavailable" ]; then + printf '\nMemory unavailable: %s\n' "$unavailable" + printf 'Dispatch continues without memory context.\n' + else + printf '\nVerified local source paths:\n' + if [ -s "$verified_file" ]; then + sed 's/^/- /' "$verified_file" + else + printf 'none\n' + fi + printf '\nWarnings:\n' + if [ -s "$warnings_file" ]; then + sed 's/^/- /' "$warnings_file" + else + printf 'none\n' + fi + fi + } >> "$brief" +} + +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/fm-memory-lookup.XXXXXX") +cleanup() { rm -rf "$TMP_DIR"; } +trap cleanup EXIT + +RAW="$TMP_DIR/raw.txt" +ERR="$TMP_DIR/err.txt" +PATHS="$TMP_DIR/paths.txt" +VERIFIED="$TMP_DIR/verified.txt" +WARNINGS="$TMP_DIR/warnings.txt" +: > "$VERIFIED" +: > "$WARNINGS" + +UNAVAILABLE= +if [ -n "$FROM_FILE" ]; then + if [ -r "$FROM_FILE" ]; then + cat "$FROM_FILE" > "$RAW" + else + UNAVAILABLE="memory input file cannot be opened: $FROM_FILE" + : > "$RAW" + fi +elif [ -z "${FM_COGNEE_LOOKUP_CMD:-}" ]; then + UNAVAILABLE="FM_COGNEE_LOOKUP_CMD is not set" + : > "$RAW" +else + case "$FM_COGNEE_LOOKUP_CMD" in + *[[:space:]]*) + UNAVAILABLE="FM_COGNEE_LOOKUP_CMD must be an executable path, not a shell command" + : > "$RAW" + ;; + *) + if [ ! -x "$FM_COGNEE_LOOKUP_CMD" ]; then + UNAVAILABLE="lookup command is not executable: $FM_COGNEE_LOOKUP_CMD" + : > "$RAW" + elif "$FM_COGNEE_LOOKUP_CMD" "$QUERY" > "$RAW" 2> "$ERR"; then + : + else + UNAVAILABLE="lookup command failed; dispatch continues without memory context" + : > "$RAW" + fi + ;; + esac +fi + +if [ -z "$UNAVAILABLE" ]; then + extract_source_paths "$PATHS" < "$RAW" + if [ -s "$PATHS" ]; then + while IFS= read -r source_path; do + if canon=$(canonical_path "$source_path" 2>/dev/null) && [ -f "$canon" ] && [ -r "$canon" ] && head -c 0 "$canon" >/dev/null 2>&1; then + printf '%s\n' "$canon" >> "$VERIFIED" + else + printf 'local source cannot be opened: %s\n' "$source_path" >> "$WARNINGS" + fi + done < "$PATHS" + else + printf 'no SOURCE_PATH references found; memory hint is unverified\n' >> "$WARNINGS" + fi +fi + +printf 'memory hint:\n' +if [ -n "$UNAVAILABLE" ]; then + printf 'memory unavailable: %s\n' "$UNAVAILABLE" +elif [ -s "$RAW" ]; then + sed -n "1,${MAX_HINT_LINES}p" "$RAW" +else + printf 'none\n' +fi + +printf '\nverified local source path:\n' +if [ -s "$VERIFIED" ]; then + sed 's/^/- /' "$VERIFIED" +else + printf 'none\n' +fi + +printf '\nwarning:\n' +if [ -n "$UNAVAILABLE" ]; then + printf -- '- %s\n' "$UNAVAILABLE" + printf -- '- dispatch continues without memory context\n' +elif [ -s "$WARNINGS" ]; then + sed 's/^/- /' "$WARNINGS" +else + printf 'none\n' +fi + +append_brief_section "$APPEND_BRIEF" "$UNAVAILABLE" "$VERIFIED" "$WARNINGS" + +# Keep this command fail-closed for authority but non-blocking for dispatch: +# operational failures are surfaced as warnings and exit 0. +exit 0 diff --git a/docs/scripts.md b/docs/scripts.md index 722b53cf15f..5a3adb077ab 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -13,6 +13,7 @@ Each file also starts with a short header comment. | `fm-ensure-agents-md.sh` | Ensure project `AGENTS.md` is the real memory file and `CLAUDE.md` symlinks to it | | `fm-guard.sh` | Warn when the primary checkout is tangled, when queued wakes are pending, or when watcher liveness is not proved by a fresh beacon plus a live matching lock | | `fm-home-seed.sh` | Lease/provision a secondmate home transactionally, clone projects, initialize gates, and maintain `data/secondmates.md` | +| `fm-memory-lookup.sh` | Manual read-only Cognee memory lookup for optional pre-dispatch hints; opens local source paths before brief attachment and stays non-blocking when unavailable | | `fm-spawn.sh` | Spawn one task, several `id=repo` pairs, or a persistent secondmate with `--secondmate`; ship/scout spawns require an isolated treehouse worktree; secondmate spawns locally sync the home before launch | | `fm-project-mode.sh` | Resolve a project's delivery mode and `+yolo` flag from `data/projects.md` | | `fm-route.sh` | Classify a task into a deterministic route profile, harness, model, effort, reason, override, and risk flags without changing spawn behavior | diff --git a/tests/fm-memory-lookup.test.sh b/tests/fm-memory-lookup.test.sh new file mode 100755 index 00000000000..a310a229bba --- /dev/null +++ b/tests/fm-memory-lookup.test.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Behavior tests for the manual Cognee memory lookup helper. +# +# The helper is intentionally optional and read-only: Cognee absence must not +# block dispatch, raw answers must stay hints, and brief attachment may include +# only local source paths that were opened successfully. +set -u + +# shellcheck source=tests/lib.sh +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +LOOKUP="$ROOT/bin/fm-memory-lookup.sh" +TMP_ROOT=$(fm_test_tmproot fm-memory-lookup) + +test_absent_cognee_is_non_blocking() { + local out rc + set +e + out=$(env -u FM_COGNEE_LOOKUP_CMD "$LOOKUP" -- "what prior report matters?") + rc=$? + set -e + expect_code 0 "$rc" "missing Cognee backend should not block dispatch" + assert_contains "$out" "memory hint:" "missing backend output should include memory hint section" + assert_contains "$out" "memory unavailable: FM_COGNEE_LOOKUP_CMD is not set" "missing backend should be explicit" + assert_contains "$out" "dispatch continues without memory context" "missing backend should tell dispatch to continue" + assert_contains "$out" "verified local source path:" "missing backend output should include source section" + pass "fm-memory-lookup: missing Cognee backend exits 0 with a visible unavailable note" +} + +test_lookup_separates_hint_verified_path_and_warning() { + local dir source_file missing_file fake out rc + dir="$TMP_ROOT/lookup" + mkdir -p "$dir" + source_file="$dir/source.md" + missing_file="$dir/missing.md" + printf 'source truth\n' > "$source_file" + fake="$dir/fake-cognee" + cat > "$fake" < "$source_file" + printf '# Task\n' > "$brief" + fake="$dir/fake-cognee" + cat > "$fake" < "$brief" + + out=$(env -u FM_COGNEE_LOOKUP_CMD "$LOOKUP" --append-brief "$brief" -- "optional lookup") + assert_contains "$out" "memory unavailable" "terminal output should say memory unavailable" + assert_grep "Memory unavailable: FM_COGNEE_LOOKUP_CMD is not set" "$brief" "brief should record memory unavailable" + assert_grep "Dispatch continues without memory context." "$brief" "brief should record non-blocking dispatch" + pass "fm-memory-lookup: unavailable Cognee can be attached as a non-blocking brief note" +} + +test_absent_cognee_is_non_blocking +test_lookup_separates_hint_verified_path_and_warning +test_brief_append_excludes_raw_hint +test_absent_cognee_brief_note