# Psi

> 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: `soul-brews-studio/psi` (Agent Skill)
- Install (CLI): `npx skillmds@latest add soul-brews-studio/psi`
- Raw SKILL.md: https://api.skillmd.com/api/skills/soul-brews-studio/psi/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: soul-brews-studio (https://skillmd.com/u/soul-brews-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/soul-brews-studio/psi

---


# /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.

```text
/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.

```bash
maw ls
```

Ask: *"Which oracle should take care of this repo's memory?"* The human answers with a
name (`neo`, `pulse`, `beta`). Resolve it — the short name works:

```bash
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.

```bash
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.

```text
  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:

```text
/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.

```bash
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/"
```

Verified against every remote shape:

| remote | slug |
|---|---|
| `https://github.com/Soul-Brews-Studio/arra-oracle-skills-cli` | `github.com/soul-brews-studio/arra-oracle-skills-cli` |
| `git@github.com:laris-co/Neo-Oracle.git` | `github.com/laris-co/neo-oracle` |
| `git@gitlab.com:MyGroup/sub/proj.git` | `gitlab.com/mygroup/sub/proj` |
| `ssh://git@git.example.com:2222/team/thing.git` | `git.example.com/team/thing` |
| `git://codeberg.org/Owner/Proj.git` | `codeberg.org/owner/proj` |

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".

```bash
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

```bash
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.

#### Record the caretaker

```bash
mkdir -p "$REPO/.claude"
printf 'oracle: %s\nrepo: %s\nvault: %s\ndate: %s\n' \
  "$ORACLE_NAME" "$CARETAKER" "$NS" "$(date +%F)" > "$REPO/.claude/PSI_CARETAKER"
rg -qx '.claude/PSI_CARETAKER' "$REPO/.gitignore" 2>/dev/null \
  || echo '.claude/PSI_CARETAKER' >> "$REPO/.gitignore"
```

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.

```bash
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.

```text
  ψ 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:

1. `PSI_CARETAKER` exists but `ψ` is missing → recreate the relative symlink.
2. `ψ` is a symlink that `readlink -f` cannot resolve → report the dangling target; do not
   invent a replacement.
3. An ignore form is missing → add it.
4. `ψ` 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.

