Attach a code repo's ψ vault to a caretaker oracle — check, link, heal, unlink. Use when a plain code repo (open-source, not an oracle) should keep memory in another oracle's vault, when the user says "psi check", "psi link", "share the vault", "symlink ψ to neo", "who takes care of this repo's memory", or when a ψ symlink went missing after a checkout. Do NOT trigger for creating an oracle (use /awaken), cloning repos for development (use /incubate), or writing a retrospective (use /rrr).
SKILL.md
/psi
"A code repo does not need to be an oracle to have a memory."
Point a plain code repo's ψ at a caretaker oracle's vault, so the repo keeps a brain
without becoming an oracle and without committing vault content into public source history.
/psi # same as check
/psi check # what is ψ here, who takes care of it, is it safe
/psi link # ask which oracle, then absorb → symlink → ignore → verify
/psi heal # symlink vanished after a checkout, or ignore rules incomplete
/psi unlink # go back to a standalone real ψ
The model
code repo/ψ ──symlink──▶ caretaker oracle/ψ
(ignored, never committed) (tracked — it IS the brain)
Repo kind
ψ
git
code repo — software someone else could clone and build
symlink to caretaker
ignored, never tracked
oracle repo — has an oracle identity
real directory
tracked
Pick the caretaker — show, don't guess
Never assume which oracle takes care of a repo. Show the fleet and let the human name
it. Linking to the wrong vault mixes two oracles' memory.
maw ls
Ask: "Which oracle should take care of this repo's memory?" The human answers with a
name (neo, , ). Resolve it — the short name works:
Installs
0
pulse
beta
maw locate "$ORACLE_NAME" # prints: repo: /path/to/<name>-oracle and ψ/: present
Take the repo: line as CARETAKER. If maw locate finds nothing, or its ψ/ is not
present, stop and say so — do not fall back to a guess.
Do not loop over repos or scan the filesystem looking for candidates. One repo, one
question, one answer.
check
Report what you verified, never what you assume. Every line is the output of a command —
readlink -f for the real target, ls-files -s for the mode bits, check-ignore -v for
the rule that actually matched. Close with one FOCUS line: the single next action.
REPO=$(git rev-parse --show-toplevel) || exit 1
GI="$REPO/.gitignore"
# ── ψ: type, size, staleness ─────────────────────────────────────────────
if [ -L "$REPO/ψ" ]; then
TARGET=$(readlink "$REPO/ψ"); REAL=$(readlink -f "$REPO/ψ")
[ -e "$REPO/ψ/" ] && ALIVE=alive || ALIVE='DANGLING'
case "$TARGET" in /*) ABS=' ⚠️ absolute';; *) ABS='';; esac
PSI="symlink → $TARGET$ABS"; PSI2="resolves $REAL ($ALIVE)"
elif [ -d "$REPO/ψ" ]; then
N=$(find "$REPO/ψ" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
NEW=$(find "$REPO/ψ" -type f ! -name '.DS_Store' -exec stat -f '%m' {} \; 2>/dev/null | sort -rn | head -1)
AGE=$(( ( $(date +%s) - ${NEW:-$(date +%s)} ) / 86400 ))
PSI="real dir · $N files · newest $(date -r "${NEW:-0}" +%F 2>/dev/null) (${AGE}d old)"; PSI2=""
else
PSI="absent"; PSI2=""
fi
# ── git: is any of it tracked? is the LINK itself committed? ─────────────
TRACKED=$(git -C "$REPO" ls-files ψ | wc -l | tr -d ' ')
git -C "$REPO" ls-files -s ψ | rg -q '^120000' && BLOB='⚠️ SYMLINK COMMITTED' || BLOB=no
# ── ignore: both forms, and the rule git actually matched ───────────────
rg -qx 'ψ' "$GI" 2>/dev/null && BARE=yes || BARE='NO ⚠️'
rg -qx 'ψ/' "$GI" 2>/dev/null && SLASH=yes || SLASH=NO
# cut -f1, NOT an awk positional field ref — see "Never use positional parameters"
# below. check-ignore -v separates source from pathname with a TAB, so field 1 is
# the matching rule.
RULE=$(git -C "$REPO" check-ignore -v ψ 2>/dev/null | cut -f1)
[ -n "$RULE" ] || RULE='NOT IGNORED ⚠️'
# ── kind + caretaker ────────────────────────────────────────────────────
[ "$TRACKED" -gt 0 ] && KIND='oracle repo (ψ is tracked — it IS the brain)' \
|| KIND='code repo (ψ must stay ignored)'
# NOTE: no \K — this rg build rejects it, and the error would silently read as "none".
CARE=$(sed -n 's/^oracle: *//p' "$REPO/.claude/PSI_CARETAKER" 2>/dev/null)
[ -n "$CARE" ] || CARE='none recorded'
# ── render: no borders. two-space margin, aligned columns, grouped by blank
# lines. identity block first, then the checks, then one FOCUS line.
# Each check names the command that proved it — the report is its own audit.
# NOTE: printf inline per row — no row()/fact() helpers, because a helper
# would need positional parameters and the host rewrites those. See below.
F=' %-14s%s\n' # identity line
R=' %-14s%-42s%-3s %s\n' # check line
echo
printf "$F" repo "$(basename "$REPO")"
printf "$F" kind "$KIND"
printf "$F" ψ "$PSI"
[ -n "$PSI2" ] && printf "$F" '' "$PSI2"
echo
printf "$R" tracked "$TRACKED entries" "$([ "$TRACKED" -eq 0 ] && echo ✓ || echo ⚠)" 'git ls-files ψ'
printf "$R" symlink-blob "$BLOB" "$([ "$BLOB" = no ] && echo ✓ || echo ⚠)" 'ls-files -s ψ → 120000'
printf "$R" bare-ψ "$BARE" "$([ "$BARE" = yes ] && echo ✓ || echo ⚠)" 'rg -qx ψ .gitignore'
printf "$R" 'ψ/' "$SLASH" "$([ "$SLASH" = yes ] && echo ✓ || echo ·)" 'rg -qx ψ/ .gitignore'
printf "$R" ignore-rule "$RULE" "$(case $RULE in *NOT*) echo ⚠;; *) echo ✓;; esac)" 'git check-ignore -v ψ'
printf "$R" caretaker "$CARE" "$([ "$CARE" = 'none recorded' ] && echo · || echo ✓)" '.claude/PSI_CARETAKER'
echo
Keep the two-space margin and the blank-line grouping — the whitespace is what makes it
readable without rules. Widen a column only if a value would otherwise wrap; never add
borders back.
Never use positional parameters (dollar-digit) in this skill's shell
The host substitutes the invocation's positional arguments into the skill body before the
model ever sees it. Running /psi link to neo but … rewrote an awk program that
referenced field one into awk '{print to}', and a four-parameter printf helper into
printf … "to" "neo" "but" "when" — silent corruption, no error.
So: no shell functions taking positional parameters, and no dollar-digit inside
awk/sed programs. Use cut -f1, a named variable, or a printf format string held in
a variable — all three are immune.
Deciding FOCUS
Emit exactly one, first match wins — most dangerous first:
Condition
FOCUS
symlink-blob committed
leaking a machine path in git history → /psi heal, then decide on a history rewrite
symlink DANGLING
brain unreachable → /psi heal
symlink target absolute
breaks on every other machine → /psi heal (rewrites relative)
code repo & bare-ψ=NO
ψ will leak the moment it is linked → add the bare rule
code repo & NOT IGNORED
do not commit → fix .gitignore first
code repo, real dir, no caretaker
orphaned vault — no oracle reads these N files → /psi link
oracle repo, ψ tracked
✅ correct — the vault belongs here, nothing to do
symlink alive, relative, ignored
✅ linked and safe
A stale newest date is worth naming even on a ✅ — a vault whose newest file is weeks old
is why a later /recap will hand back a stale handoff as if it were current.
link — a six-phase ritual
Run it step by step, not as one script. Announce each phase as you enter it, do that
phase's work, show the result, then continue. Two phases are hard gates: stop and wait
for a human answer. After the phases, run the checklist, then print the report.
Phase 1/6 · Survey what ψ is now
Phase 2/6 · Caretaker ← GATE: the human names the oracle
Phase 3/6 · Preview ← GATE: the human approves the dry run
Phase 4/6 · Absorb copy + count both sides
Phase 5/6 · Link park → symlink → verify through it
Phase 6/6 · Seal ignore rules + caretaker record
Never run a later phase's commands while announcing an earlier one. A gate that is
"announced and then passed in the same breath" is not a gate.
Phase 1/6 · Survey — refuse bad ground
Run check first and show it. Then stop, with the reason, when:
the repo is itself an oracle (ψ tracked with real content),
ψ already points at that same caretaker → say already linked, exit 0,
there is no origin remote (the vault path cannot be derived).
Phase 2/6 · Caretaker — GATE
Show maw ls, then ask which oracle should take care of this repo's memory and wait for
the answer. Resolve it with maw locate, and confirm the caretaker's ψ/: present plus
that it is not behind its remote. Never pick a name yourself, never carry one over from a
previous run.
Phase 3/6 · Preview — GATE
Show the itemized dry run and the counts, then wait for approval. Dry-run output is not
consent. If the human says nothing, nothing happens.
Phase 4/6 · Absorb — dry run first, always
Replacing a populated ψ with a symlink orphans everything inside it. Fetch the caretaker
first so a stale vault is not merged over newer content.
Every linked repo lives under ψ/family/<host>/<owner>/<repo>, always lowercased.
Below family/ the path is the ghq tree exactly, so it reads the same in both places:
/opt/Code/github.com/soul-brews-studio/arra-oracle-skills-cli ← the code (ghq)
neo-oracle/ψ/family/github.com/soul-brews-studio/arra-oracle-skills-cli ← its memory
└ family ┘└─ host ──┘└─ owner ───────┘└─ repo ────────────┘
family/ keeps the oracle's kin clear of its own organs. Without it an owner directory
lands beside memory/, inbox/, teams/ — and an org literally named teams collides
with the oracle's own. One directory to list to see everything an oracle tends.
The host is derived, never assumed.gitlab.com, codeberg.org, or a self-hosted
git.example.com each get their own subtree, so two repos sharing an owner/name on
different hosts never collide.
Lowercase is not cosmetic either: Soul-Brews-Studio and soul-brews-studio are the same
repo, but on a case-sensitive volume they become two vaults, and the split is invisible
until memory goes missing.
git -C "$CARETAKER" fetch --quiet 2>/dev/null
# host/owner/repo from the origin remote, lowercased — never from the directory
# name, which may have been renamed locally. Handles https, ssh, scp-style,
# git://, an embedded user, a custom port, and gitlab subgroups.
URL=$(git -C "$REPO" remote get-url origin 2>/dev/null)
[ -n "$URL" ] || { echo "✗ no origin remote — cannot derive the vault path"; exit 1; }
SLUG=$(printf '%s' "$URL" | sed -E \
-e 's#^(ssh|git\+ssh|https?|git)://##' \
-e 's#^[^@/]+@##' \
-e 's#:[0-9]+/#/#' \
-e 's#:#/#' \
-e 's#\.git$##' \
-e 's#/+$##' \
| tr '[:upper:]' '[:lower:]')
NS="$CARETAKER/ψ/family/$SLUG" # ψ/family/<host>/<owner>/<repo>, lowercase
rsync -a --dry-run --itemize-changes "$REPO/ψ/" "$NS/"
Print the itemized list and the file count, then wait for approval. Only then re-run
without --dry-run. Never --delete.
The absorb and the removal are never the same command, and never the same step. Step 3
sequences them: copy → count both sides → park the original → link → verify through the
link, with an automatic rollback if the last check fails.
Phase 5/6 · Link — copy, verify, park, link, verify again
Order is the safety. The source is never removed; it is parked outside the repo and
only after the copy has been counted. If the link fails to resolve, roll back automatically.
Never rm -rf the vault — see "nothing deleted".
mkdir -p "$NS"
# 3a. copy for real (same command as the dry run, minus --dry-run)
rsync -a --exclude '.DS_Store' "$REPO/ψ/" "$NS/"
# 3b. VERIFY before anything moves. Counts must match exactly, or stop —
# the source is still untouched at this point, so aborting costs nothing.
SRC_N=$(find "$REPO/ψ" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
DST_N=$(find "$NS" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
if [ "$SRC_N" -ne "$DST_N" ]; then
echo "✗ ABORT — copied $DST_N of $SRC_N files. Source untouched; nothing removed."
exit 1
fi
echo "✓ verified $DST_N/$SRC_N files copied"
# 3c. PARK the original outside the repo (never delete, never leave it in git's way)
PARK="${TMPDIR:-/tmp}/psi-replaced-$(echo "$SLUG" | tr / -)-$(date +%Y%m%d-%H%M%S)"
mv "$REPO/ψ" "$PARK"
# 3d. link, relative
REL=$(python3 -c 'import os,sys;print(os.path.relpath(sys.argv[1],sys.argv[2]))' "$NS" "$REPO")
ln -sfn "$REL" "$REPO/ψ" # -n: do not descend into an existing symlink
# 3e. verify the link RESOLVES and the content is reachable THROUGH it.
# Any failure restores the parked original and exits.
restore() { rm -f "$REPO/ψ"; mv "$PARK" "$REPO/ψ"; echo "↩ restored original ψ"; }
[ -e "$REPO/ψ/" ] || { echo "✗ link does not resolve"; restore; exit 1; }
THRU=$(find "$REPO/ψ/" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
[ "$THRU" -ge "$SRC_N" ] || { echo "✗ only $THRU/$SRC_N reachable through the link"; restore; exit 1; }
readlink -f "$REPO/ψ"
echo "✓ $THRU files reachable through the link"
echo " original parked: $PARK (delete it yourself once satisfied)"
ln -sfn, never ln -sf — with a pre-existing symlinked directory -f alone creates the
link inside the target instead of replacing it.
Why park instead of delete: at 3c the content exists in two places and has been counted
in both, so parking looks redundant — until 3d writes a link that silently resolves
somewhere unexpected. Parking makes 3e's rollback possible. Announce the park path; let the
human delete it. Nothing deleted.
Phase 6/6 · Seal — ignore it, prove it, record the caretaker
for pat in 'ψ' 'ψ/'; do
rg -qx "$pat" "$REPO/.gitignore" 2>/dev/null || echo "$pat" >> "$REPO/.gitignore"
done
if git -C "$REPO" ls-files --error-unmatch ψ >/dev/null 2>&1; then
git -C "$REPO" rm --cached -r --quiet ψ
echo "✓ untracked previously-committed ψ (history still holds it — rewrite separately)"
fi
git -C "$REPO" check-ignore -v ψ || echo "⚠️ ψ is NOT ignored — do not commit"
Verification is part of the step. If check-ignore prints nothing, the link is not safe —
report that instead of declaring success.
Machine-local, ignored like the link. check reads it to name the caretaker without asking
again.
Checklist — run it, show it, and let it fail loudly
After phase 6, verify the outcome. Every line re-runs a real command; nothing is asserted
from memory of what the phases intended to do. Print it. Unlike the retro checklist in
/rrr, this one is shown — it is the evidence that a data move succeeded.
C=' %-2s %-34s%s\n' # format in a variable — no helper, no positional params
# 1 counts match on both sides
SRC_OK=$([ "${SRC_N:-0}" -eq "$DST_N" ] && echo ✓ || echo ✗)
printf "$C" "$SRC_OK" "files copied" "$DST_N/$SRC_N"
# 2 reachable THROUGH the link, not just present in the vault
printf "$C" "$([ "${THRU:-0}" -ge "${SRC_N:-0}" ] && echo ✓ || echo ✗)" "reachable through ψ" "$THRU"
# 3 the link resolves
printf "$C" "$([ -e "$REPO/ψ/" ] && echo ✓ || echo ✗)" "link resolves" "$(readlink -f "$REPO/ψ")"
# 4 relative, not absolute
case "$(readlink "$REPO/ψ")" in /*) A=✗;; *) A=✓;; esac
printf "$C" "$A" "relative target" "$(readlink "$REPO/ψ")"
# 5 lowercase, nested owner/repo
printf "$C" "$([ "$SLUG" = "$(echo "$SLUG" | tr '[:upper:]' '[:lower:]')" ] && echo ✓ || echo ✗)" "vault path lowercase" "ψ/$SLUG"
# 6 git cannot see it
printf "$C" "$(git -C "$REPO" check-ignore -q ψ && echo ✓ || echo ✗)" "ignored by git" "$(git -C "$REPO" check-ignore -v ψ | cut -f1)"
# 7 the link itself is not committed
printf "$C" "$(git -C "$REPO" ls-files -s ψ | rg -q '^120000' && echo ✗ || echo ✓)" "symlink not committed" "$(git -C "$REPO" ls-files ψ | wc -l | tr -d ' ') tracked"
# 8 the original still exists somewhere
printf "$C" "$([ -d "$PARK" ] && echo ✓ || echo ✗)" "original parked" "$PARK"
Any ✗ means stop and say so — do not print a success report over a failed checklist.
Items 2 and 8 are the ones that matter most: 2 proves the brain is actually reachable, 8
proves nothing was destroyed.
Report
Close with a short report in the same borderless style as check. State what moved, where
it went, and what the human still owns.
ψ linked · arra-oracle-skills-cli → neo
moved 17 files → neo-oracle/ψ/family/github.com/soul-brews-studio/arra-oracle-skills-cli
symlink ../../laris-co/neo-oracle/ψ/family/github.com/soul-brews-studio/arra-oracle-skills-cli
ignored .gitignore:9:ψ (bare rule — ψ/ no longer matches a symlink)
caretaker neo
parked /var/.../psi-replaced-…-20260820-181051
yours to do delete the parked copy once satisfied
commit the .gitignore change
Report only what the checklist verified. If an item failed, the report says what failed and
what state the repo is in — never a clean summary over a broken outcome.
heal
git checkout and git merge silently delete an ignored symlink when they remove the
last tracked sibling at that path — no error is printed. Repair, reporting each fix:
PSI_CARETAKER exists but ψ is missing → recreate the relative symlink.
ψ is a symlink that readlink -f cannot resolve → report the dangling target; do not
invent a replacement.
An ignore form is missing → add it.
ψ is tracked → git rm --cached -r ψ.
Print ✓ nothing to heal when clean.
unlink
Copy the namespaced subtree back into a real ψ, replace the symlink, and leave the
caretaker's copy alone — never delete the vault side. Keep the ignore lines unless asked.
Rules
Show maw ls and let the human name the caretaker. Never guess, never scan for one.
Ask before the first destructive step; dry-run output is not consent.
Nothing deleted. Never rm -rf a vault. Copy → count both sides → park the original
outside the repo → link → verify through the link, rolling back on any failure. The human
deletes the parked copy, not the skill.
Both ψ and ψ/ in .gitignore, every time — a trailing slash matches directories
only, so a ψ/-only rule stops matching the moment ψ becomes a symlink.
Relative symlink targets only — machines disagree on the ghq root.
Never commit ψ or the symlink in a code repo; never un-track ψ in an oracle repo.
A shared vault is shared: give each repo its own subtree, and say so when linking.
Report what you verified (readlink -f, check-ignore), not what you intended.