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
181 changes: 181 additions & 0 deletions .claude/skills/recall/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
name: recall
type: command
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Missing delta check note 📜 Skill insight ≡ Correctness

This PR adds new files under .claude/skills/, which triggers the requirement to run an alignment
delta check and document any sibling follow-up needs in the review reply. The PR should not be
merged until the delta check result is explicitly noted.
Agent Prompt
## Issue description
This PR modifies `.claude/skills/`, so an alignment delta check must be run and the results (including any sibling repo needing a follow-up PR) must be noted in the review reply.

## Issue Context
Compliance requires an explicit statement in the review reply that `workflow.sh delta` (or equivalent) was run and what siblings, if any, require follow-up.

## Fix Focus Areas
- .claude/skills/recall/SKILL.md[1-3]
- .claude/skills/remember/SKILL.md[1-3]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

description: >
Search the shared eidetic memory store and get back ranked, provenanced
records. Drives `eidetic recall` with four search modes — exact (verbatim
substring), approximate (vector/semantic), keyword (BM25 lexical), and hybrid
(a weighted blend of vector+keyword, the default) — each hit carrying its
text, full metadata, a relevance `score`, and a freshness `signal`. Recall
passively reinforces matched records (bumps last_recall + recall_count).
Shadowed and archived records are excluded by default; use
--include-shadowed / --include-archived to retrieve them. The store lives at
~/.eidetic/memory (a home-dir path outside any git worktree); the wrapper
defaults queries to this agent's PERSONAL, PRIVATE scope (`--scope tensor-cli
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Portability lint failure 🐞 Bug ☼ Reliability

The new SKILL docs hard-code ~/.eidetic/memory, which matches the repo’s portability-lint rule
that flags ~/.<dotfile> references in committed .md/config files. Running
.claude/skills/cicd/scripts/portability-lint.sh on this PR will fail.
Agent Prompt
## Issue description
The repo’s portability lint flags `~/.*` dotfile references in committed markdown/config files. The new memory skills document the store path as `~/.eidetic/memory`, which will be flagged.

## Issue Context
`portability-lint.sh` searches modified `.md|.yaml|.toml|.json` files for `~/\.[A-Za-z]` and fails unless the path matches one of a couple carve-outs.

## Fix Focus Areas
- .claude/skills/recall/SKILL.md[11-14]
- .claude/skills/remember/SKILL.md[11-13]
- .claude/skills/cicd/scripts/portability-lint.sh[26-35]

## Suggested fix approach
Change docs to use a portable form that doesn’t trip the lint, e.g. `$HOME/.eidetic/memory` (or `${HOME}/.eidetic/memory`) instead of `~/.eidetic/memory`. If you intentionally want to allow this dotdir literal, add a carve-out in `portability-lint.sh` for `~/.eidetic/`, but prefer the doc change to keep the lint strict.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

--visibility private`, suffix read from culture.yaml) — matching where
/remember writes — so a no-flag recall returns this agent's own private records
plus the shared public pool, and Claude and the colleague backend recall each
other's memories because both resolve the same suffix via this skill. Use
when the user says "recall", "what do we know about X", "search memory",
"have we seen X before", "look it up in memory", "eidetic recall", or before
answering from scratch when prior context may already be stored. Pairs with
the sibling /remember skill.
---

# recall — search the shared eidetic memory

`recall` drives **`eidetic recall`**: given a query, it returns the top-k stored
records ranked by relevance, each with its `text`, full `metadata` (provenance),
a numeric `score`, and a freshness `signal`. It is the read half of the memory
surface; the write half is the sibling **/remember** skill.

The point of a *shared* store is that memory is a **team faculty**, not a
per-agent silo: a record Claude wrote is recallable by the colleague backend
(and vice versa), because both resolve the same `~/.eidetic/memory` path.

## How to run

```bash
bash .claude/skills/recall/scripts/recall.sh "<query>" [flags...]
```

The wrapper resolves the CLI portably (installed `eidetic` on `PATH`, else
`uv run eidetic` from the checkout) and forwards every flag verbatim, so it is
exactly `eidetic recall …`. Run it from anywhere; the store is the same.

## Search modes (`--mode`, default `hybrid`)

| Mode | What it matches | Needs embed server? |
|------|-----------------|---------------------|
| `exact` | case-insensitive verbatim substring (`--case-sensitive` to tighten) | no — offline-safe |
| `approximate` | vector cosine / semantic similarity | yes (falls back offline) |
| `keyword` | BM25 lexical; only records sharing a query term | no — offline-safe |
| `hybrid` | `alpha*approximate + (1-alpha)*keyword` (`--alpha`, default 0.5) | uses it when up |

`hybrid` is the default because the two signals cover each other's blind spots:
vector catches paraphrases, keyword catches exact ids/quotes. When the embed
server is unreachable, `hybrid` collapses to keyword-only (it never fuses
meaningless offline-fallback cosine).

## Output fields

Each hit in `--json` output includes:

| Field | Notes |
|-------|-------|
| `id` | stable record identity |
| `text` | the stored chunk |
| `type` | record type |
| `metadata` | full provenance, round-tripped verbatim from ingest |
| `score` | relevance score from the chosen search mode (freshness-blended) |
| `signal` | freshness strength in [0, 1]; computed at recall time from age, recall frequency, and staleness |
| `created` | ISO-8601 ingest date (may be DATE_UNKNOWN for legacy records) |
| `last_recall` | ISO-8601 timestamp of the most recent recall hit (null if never recalled) |
| `recall_count` | number of times this record has been recalled (passive reinforcement counter) |
| `lifecycle` | `active`, `shadowed`, or `archived` |
| `links` | list of related-memory ids |

## Freshness signal

Every `recall` hit carries a `signal` field (float in `[0, 1]`). The signal
blends **multiplicatively** into the lexical/vector score so recently-created
and frequently-recalled records surface ahead of stale ones. The formula:

```
access_bonus = min(0.5, recall_count * 0.05)
age_factor = 1 / (1 + days_since_creation * 0.01)
staleness = days_since_last_recall * 0.01
signal = clamp((0.5 - staleness + access_bonus) * age_factor, 0, 1)
blended_score = score * (1 + 0.25 * (signal - 0.5))
```

Records with no temporal data (legacy, undated) are an exact no-op — the blend
is skipped for them so pre-existing fixture scores are unchanged.

Each `recall` call is also **passive reinforcement**: it bumps `last_recall` and
`recall_count` on every matched record, so frequently-recalled memories organically
gain signal strength over time.

## Lifecycle flags

By default, `recall` returns only `active` records. Use these flags to retrieve
non-active records:

- `--include-shadowed` — include records whose `lifecycle == "shadowed"` (records
superseded within their scope by a newer record). Shadowed records are preserved
and still searchable; they are just hidden from the default result set.
- `--include-archived` — include records whose `lifecycle == "archived"` (records
older than ~1 year or below the signal threshold). Archived records are fully
preserved; the flag makes them retrievable again.

Both flags can be combined. Neither affects ranking — shadowed/archived records
compete on score/signal just like active ones when included.

## Common flags (forwarded to `eidetic recall`)

- `--mode exact|approximate|keyword|hybrid` — default `hybrid`.
- `--top-k N` — max results (default 5).
- `--alpha F` — hybrid blend weight in `[0,1]` (default 0.5).
- `--case-sensitive` — for `--mode exact`.
- `--filter KEY=VALUE` — metadata facet filter (repeatable): e.g. `--filter source=docs`.
- `--scope NAME` / `--visibility public|private` — scope isolation (no private
leak). **The wrapper defaults this to the agent's PERSONAL, PRIVATE scope**
(`--scope tensor-cli --visibility private`, suffix read from `culture.yaml`),
matching where `/remember` writes — so a no-flag recall returns this agent's
own private records **plus** the shared public pool, while those private records
stay invisible to a `default`/other-scope recall. Pass `--scope`/`--visibility`
to query elsewhere; a wheel install with no `culture.yaml` falls back to the
CLI default `default`/`public`.
- `--backend files|mongo|neo4j` — default `files` (the shared home-dir store).
- `--include-shadowed` — include shadowed records in results (excluded by default).
- `--include-archived` — include archived records in results (excluded by default).
- `--json` — structured list to stdout (use this when an agent parses the result).

## Examples

```bash
# Default hybrid recall, JSON for an agent to parse:
bash .claude/skills/recall/scripts/recall.sh "jetson nano power draw" --json

# Find the exact message that mentions a phrase:
bash .claude/skills/recall/scripts/recall.sh "Orin Nano" --mode exact

# Keyword search, offline-safe, narrowed to a source:
bash .claude/skills/recall/scripts/recall.sh "thermal throttle" --mode keyword \
--filter source=discord --top-k 10

# Retrieve a record that was recently shadowed (its superseding record is now active):
bash .claude/skills/recall/scripts/recall.sh "old topic" --include-shadowed --json

# Retrieve all records including archived (to audit stale memories):
bash .claude/skills/recall/scripts/recall.sh "power" --include-archived --include-shadowed --json
```

## Notes

- **Provenance is mandatory** on every hit — recall is for *cited* answers.
- The embed endpoint defaults to the local model-gear embed gear
(`http://localhost:8002/v1`, model `Qwen/Qwen3-Embedding-0.6B`); override with
`EIDETIC_EMBED_URL` / `EIDETIC_EMBED_MODEL`. `exact`/`keyword` ignore it.
- **Use the wrapper, not a bare `eidetic`.** The console script may not be on
`PATH` (in a dev checkout it isn't) — the wrapper resolves it for you (`PATH`
first, else `uv run eidetic`). For the docs, run `eidetic explain recall` if
installed, otherwise `uv run --project <eidetic-cli checkout> eidetic explain
recall`. (`explain` is an **`eidetic`** verb — a sibling tool like `devex`
won't know it.)
- **Reading scores:** `exact`, `keyword`, and `hybrid` drop non-matching records
(hybrid drops any record with a `0.0` blended score), so their hits are real
matches. `approximate` keeps every candidate ranked by raw cosine, so it can
return low/near-zero scores when the store is small — lower `--top-k` to trim.
A `--min-score` threshold is a tracked follow-up.
- **Sharing scope = one OS user.** The default store is `~/.eidetic/memory`, so
every agent/process running as the *same* OS user shares it (that is the point —
Claude + colleague). It is not isolated between OS users by anything but file
permissions; keep genuinely private data in a `--visibility private` scope and
treat the host as the trust boundary.

## Provenance

First-party to **eidetic-cli** — eidetic owns its memory surface. Cite, don't
import: downstream repos copy this skill, they don't symlink it. See
[`docs/skill-sources.md`](../../../docs/skill-sources.md).
141 changes: 141 additions & 0 deletions .claude/skills/recall/scripts/recall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# recall.sh — search the shared eidetic memory store (the /recall skill).
#
# Thin, portable wrapper around `eidetic recall`. It resolves the CLI, points
# the embedding modes at the local model-gear embed gear (overridable), and
# forwards every flag verbatim — so `recall.sh "<query>" --mode hybrid --json`
# is exactly `eidetic recall "<query>" --mode hybrid --json`.
#
# The store is the files backend at ~/.eidetic/memory by default — a home-dir
# path OUTSIDE any git worktree, so Claude and the colleague backend (which runs
# in throwaway worktrees) read the SAME memories. Set EIDETIC_DATA_DIR to opt out
# of sharing; set EIDETIC_MONGO_URI / NEO4J_URI + --backend for a server store.

set -euo pipefail

# ── resolve the eidetic CLI (installed tool first, then dev checkout) ────────
EIDETIC=()
resolve_eidetic() {
if command -v eidetic >/dev/null 2>&1; then
EIDETIC=(eidetic) # installed console script — the normal case
return 0
fi
# Dev fallback: inside the eidetic-cli checkout, run via uv.
local dir
dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
if [ -f "$dir/pyproject.toml" ] \
&& grep -q '^name = "eidetic-cli"' "$dir/pyproject.toml" 2>/dev/null; then
if command -v uv >/dev/null 2>&1; then
EIDETIC=(uv run --project "$dir" eidetic)
return 0
fi
break
fi
dir=$(dirname "$dir")
done
cat >&2 <<'EOF'
error: eidetic CLI not found.
hint: install it with `uv tool install eidetic-cli` (or `pipx install eidetic-cli`),
or run from inside the eidetic-cli checkout with `uv` available.
The console script is `eidetic` (dist name: eidetic-cli).
EOF
return 1
}

usage() {
cat <<'EOF'
recall.sh — search the shared eidetic memory store (the /recall skill).

Usage:
recall.sh "<query>" [--mode exact|approximate|keyword|hybrid] [--top-k N] \
[--alpha F] [--case-sensitive] [--filter KEY=VALUE]... \
[--backend files|mongo|neo4j] [--scope NAME] [--visibility public|private] \
[--json]

Modes (default: hybrid):
exact case-insensitive verbatim substring (--case-sensitive to tighten); offline-safe
approximate vector cosine / semantic similarity (uses the embed server)
keyword BM25 lexical; only records sharing a query term; offline-safe
hybrid alpha*approximate + (1-alpha)*keyword (--alpha, default 0.5);
degrades to keyword-only when the embed server is offline

Every flag is forwarded verbatim to `eidetic recall`. See `eidetic explain recall`.
EOF
}

case "${1:-}" in
-h | --help | help | "")
usage
exit 0
;;
esac

resolve_eidetic || exit 2

# ── default to this agent's PERSONAL, PRIVATE scope (culture.yaml `suffix`) ──
# Query this agent's OWN personal scope by default, matching where /remember
# writes, instead of the global `default` scope shared by every project on this
# host. We read the `suffix` from the nearest culture.yaml (walking up from this
# script), so the scope follows the repo identity rather than being hard-coded —
# a downstream cite-don't-import copy adapts to its own suffix, and the colleague
# backend (running in a worktree of this same repo) resolves the same suffix,
# keeping the Claude↔colleague shared-memory story intact.
#
# The personal scope is PRIVATE by default to match /remember: in eidetic's model
# a private record is served only to a recall in the SAME scope (`can_serve`), so
# querying with --scope <suffix> --visibility private is what retrieves those
# isolated records (a public/default recall can't see them). Scope and visibility
# are paired — the private default applies only when we inject the resolved scope,
# and only if the caller didn't pass --visibility (so an explicit
# `--visibility public` still wins). An explicit --scope on the command line takes
# over steering entirely; a wheel install with no culture.yaml falls back to the
# plain CLI default (`default`/`public`).
resolve_scope() {
local dir suffix=""
dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
if [ -f "$dir/culture.yaml" ]; then
# Capture only the first non-space token after `suffix:` (so an
# inline `# comment` or trailing space can't bleed into the scope),
# then strip surrounding quotes only — matching the canonical parser
# in .claude/skills/cicd/scripts/_resolve-nick.sh.
suffix=$(sed -n \
's/^[[:space:]]*-\{0,1\}[[:space:]]*suffix:[[:space:]]*\([^[:space:]]*\).*/\1/p' \
"$dir/culture.yaml" | head -n1 | tr -d "\"'")
break
fi
dir=$(dirname "$dir")
done
printf '%s' "$suffix"
}

has_flag() {
local needle=$1
shift
local a
for a in "$@"; do
case "$a" in
"$needle" | "$needle"=*) return 0 ;;
esac
done
return 1
}

SCOPE_ARGS=()
if ! has_flag --scope "$@"; then
EIDETIC_SCOPE=$(resolve_scope)
if [ -n "$EIDETIC_SCOPE" ]; then
SCOPE_ARGS+=(--scope "$EIDETIC_SCOPE")
has_flag --visibility "$@" || SCOPE_ARGS+=(--visibility private)
fi
fi

# Default the embedding endpoint to the local model-gear embed gear. eidetic
# falls back to a deterministic offline embedding if it's unreachable, so this
# is safe even when the gear is down. Override by exporting these yourself.
: "${EIDETIC_EMBED_URL:=http://localhost:8002/v1}"
: "${EIDETIC_EMBED_MODEL:=Qwen/Qwen3-Embedding-0.6B}"
export EIDETIC_EMBED_URL EIDETIC_EMBED_MODEL

exec "${EIDETIC[@]}" recall "${SCOPE_ARGS[@]}" "$@"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. recall.sh forwards exit codes 📘 Rule violation ≡ Correctness

recall.sh and remember.sh use exec to run eidetic, which causes the scripts to pass through
whatever exit code eidetic (or a set -e failure) returns, potentially producing exit codes other
than 0, 1, or 2. This violates the exit-code restriction requirement for entry-point scripts.
Agent Prompt
## Issue description
The `recall.sh` and `remember.sh` entry-point wrapper scripts can exit with arbitrary codes because they `exec` `eidetic` directly, and `set -euo pipefail` can also cause propagation of non-`0`/`1`/`2` exit codes.

## Issue Context
Compliance requires process exit codes to be restricted to `0`, `1`, or `2` only.

## Fix Focus Areas
- .claude/skills/recall/scripts/recall.sh[14-141]
- .claude/skills/remember/scripts/remember.sh[21-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Loading
Loading