Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.
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
236 changes: 236 additions & 0 deletions bin/fm-memory-lookup.sh
Original file line number Diff line number Diff line change
@@ -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] -- <question>

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
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
99 changes: 99 additions & 0 deletions tests/fm-memory-lookup.test.sh
Original file line number Diff line number Diff line change
@@ -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" <<SH
#!/usr/bin/env bash
printf '%s\n' 'Cognee says this old report may help. SOURCE_PATH=$source_file'
printf '%s\n' 'Another generated citation SOURCE_PATH=$missing_file'
SH
chmod +x "$fake"

set +e
out=$(FM_COGNEE_LOOKUP_CMD="$fake" "$LOOKUP" -- "find context")
rc=$?
set -e
expect_code 0 "$rc" "configured lookup should exit 0"
assert_contains "$out" "memory hint:" "lookup output should include hint section"
assert_contains "$out" "Cognee says this old report may help" "lookup output should show the advisory hint"
assert_contains "$out" "verified local source path:" "lookup output should include verified path section"
assert_contains "$out" "- $source_file" "opened source path should be listed"
assert_contains "$out" "warning:" "lookup output should include warning section"
assert_contains "$out" "local source cannot be opened: $missing_file" "missing source should warn"
pass "fm-memory-lookup: hint, opened local source path, and warning stay separate"
}

test_brief_append_excludes_raw_hint() {
local dir source_file fake brief out
dir="$TMP_ROOT/brief"
mkdir -p "$dir"
source_file="$dir/source.md"
brief="$dir/brief.md"
printf 'source truth\n' > "$source_file"
printf '# Task\n' > "$brief"
fake="$dir/fake-cognee"
cat > "$fake" <<SH
#!/usr/bin/env bash
printf '%s\n' 'RAW COGNEE ANSWER MUST NOT ENTER THE BRIEF. SOURCE_PATH=$source_file'
SH
chmod +x "$fake"

out=$(FM_COGNEE_LOOKUP_CMD="$fake" "$LOOKUP" --append-brief "$brief" -- "attach context")
assert_contains "$out" "RAW COGNEE ANSWER" "terminal output may show the advisory hint"
assert_grep "# Optional memory lookup" "$brief" "brief should get memory section"
assert_grep "$source_file" "$brief" "brief should include opened source path"
assert_grep "Cognee hints are not proof" "$brief" "brief should keep authority warning"
assert_no_grep "RAW COGNEE ANSWER" "$brief" "brief must not include raw Cognee answer as proof"
pass "fm-memory-lookup: brief attachment includes verified paths, not raw hints"
}

test_absent_cognee_brief_note() {
local dir brief out
dir="$TMP_ROOT/unavailable-brief"
mkdir -p "$dir"
brief="$dir/brief.md"
printf '# Task\n' > "$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