Add CLAUDE.md, collection scaffold, and MemPalace wing

Establish this repo as a collection of independent script folders for the
self-hosted environment (odin, an Unraid host).

- CLAUDE.md: repo conventions, the odin stack, and MemPalace usage. The
  infrastructure facts are carried over from the FamilySync project, where
  they are documented and verified; the mapping of the name "odin" to that
  host is assumed and flagged for confirmation, along with the SSH/deploy
  gaps marked "?".
- Core rule: each collection is a self-contained top-level folder owning its
  own docs, config, and dependencies. No shared/ or utils/ at the root —
  duplication is preferred over coupling so a collection stays independently
  deletable.
- _template/: scaffold making that rule concrete. The bash entrypoint ships
  strict mode, --dry-run, and a required-env guard (all four paths tested).
- mempalace.yaml: wing "odin-scripts", set explicitly because basename
  auto-detection would produce the colliding wing "scripts". Tracked rather
  than gitignored so a fresh clone keeps the config; entities.json stays
  ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-08-15 20:24:16 -04:00
co-authored by Claude Opus 5
parent 4437ff5e54
commit c6be0dc9d6
7 changed files with 355 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Secrets — every collection keeps its own .env beside its .env.example
.env
**/.env
!**/.env.example
# MemPalace per-project files (issue #185).
# `mempalace init` ignores both by default. We deliberately track mempalace.yaml —
# it is just wing/room config, and losing it on a fresh clone would make mining
# fall back to basename auto-detection (wing "scripts", which collides).
# entities.json stays ignored: it holds detected people/entities.
entities.json
+197
View File
@@ -0,0 +1,197 @@
# scripts
Operational scripts for Luc's self-hosted environment. Each script collection is an
independent, self-documenting folder at the repo root. There is no shared runtime, no
build step, and no top-level package manifest — this is a repo of standalone tools, not
an application.
---
## The one hard rule: collections are self-contained
Every script collection lives in **its own top-level folder** and carries **all of its own
documentation, configuration, and dependencies**. A collection must be understandable,
runnable, and deletable on its own.
```
scripts/
├── CLAUDE.md # this file — repo-wide context
├── README.md # human-facing index of collections
├── _template/ # scaffold for a new collection (leading _ = not a collection)
└── <collection>/
├── README.md # REQUIRED — what it does, prerequisites, usage
├── CLAUDE.md # OPTIONAL — agent context, only if non-obvious
├── .env.example # REQUIRED if the collection reads secrets
├── bin/ # executable entrypoints (chmod +x, shebang)
├── lib/ # sourced helpers, not directly executable
└── docs/ # anything longer than the README warrants
```
**Do not** create shared/, common/, or utils/ at the repo root. If two collections need
the same helper, copy it. Duplication across collections is explicitly preferred over
coupling — a collection that depends on a sibling is no longer independently deletable,
and these scripts get deployed to different places at different times.
Folders prefixed with `_` are repo infrastructure, not collections.
### Adding a collection
Copy `_template/` to the new name, fill in its README, and add one line to the root
`README.md` index. That is the whole process.
---
## The environment
> **⚠ Confirm the gaps marked `?` below, then delete this warning.** The infrastructure
> facts are carried over from the FamilySync project, where they are documented and
> verified. Only the mapping of the *name* `odin` to that host is assumed — the name
> appears in no memory, transcript, or config on this box.
**odin** — the Unraid server; the single host everything self-hosted runs on. Verified
characteristics:
| Aspect | Detail |
| --- | --- |
| Host OS | Unraid |
| Container runtime | Docker + Docker Compose (stacks defined per-app) |
| Database | MariaDB 11.x — **PostgreSQL is not available**, treat as a hard constraint |
| Cache / queue | Redis (available, used optionally) |
| Auth | Authelia — OIDC/OAuth2 provider for internal apps |
| Ingress | Pangolin/Newt tunnel — **no open inbound ports** |
| DNS | Split-DNS on `bergerhouse.net`; private IPs resolve internally |
| Git forge | Self-hosted Gitea/Forgejo at `git.bergerhouse.net` (user `luckberg`) |
| CI | Gitea Actions with a self-hosted runner |
| Mail / calendar | Fastmail (paid) — JMAP/CalDAV, the source of truth for calendars |
Known service hostnames: `git.bergerhouse.net`, `familysync.bergerhouse.net`,
`familysync-dev.bergerhouse.net`.
**Not on odin:** GitHub (`gh` is not installed — use `tea` for the Gitea forge),
PostgreSQL, any cloud provider.
### This box vs. odin
The machine this repo is edited on is `dev` — a **separate workstation**, not the server.
Scripts written here usually *target* odin (over SSH, or by being deployed into a
container/user-script on it). Never assume a script runs on the same host it was authored
on: take the target as a parameter or read it from config.
`?` — SSH access method to odin (no `~/.ssh/config` entry exists on this box yet), and
where deployed scripts are expected to live on the Unraid host.
---
## Prior context: MemPalace
Much of what is known about this environment lives in **MemPalace**, a local semantic
memory store — not in this repo. Query it before asking Luc to re-explain something, and
before assuming an infrastructure detail is undocumented.
| Aspect | Detail |
| --- | --- |
| Version | `mempalace` v3.4.1, installed as a `uv` tool |
| Palace path | `/home/luc/.mempalace/palace` |
| Backend | ChromaDB (vector search) + SQLite (metadata); fully local, no API key |
| Access | MCP server (`mempalace-mcp`) → 19 `mempalace_*` tools; plus the `mempalace` CLI |
| Size | ~32k drawers |
**Read the docs, don't guess the CLI.** MemPalace ships its own instructions:
```bash
mempalace instructions <help|init|mine|search|status>
```
Structure is **Wings → Rooms → Closets → Drawers** — wings are projects/people, rooms are
topics, closets are summaries, drawers are verbatim memories. *Halls* connect rooms within
a wing; *tunnels* connect rooms across wings.
Existing wings: `familysync` (~21.6k), `sessions` (~10.6k — mined agent transcripts),
`wing_familysync`.
### This repo's wing
Initialized with `mempalace init . --yes` and mined with `mempalace mine .`.
- **Wing: `odin-scripts`** — set explicitly in `mempalace.yaml`, *not* auto-detected.
Auto-detection uses the directory basename, and a wing called `scripts` would collide
with any other corpus in a folder of that name.
- **Rooms:** `environment`, `documentation`, `collections`, `general`.
- Room routing matches keywords against the file **path**, so never use `scripts` as a
room keyword here — it matches this repo's own path and swallows every file. (Learned
the hard way; the comment in `mempalace.yaml` says so too.)
- `mempalace.yaml` and `entities.json` are gitignored — that is MemPalace's own
convention (issue #185), applied by `init`.
Re-mine after adding a collection: `mempalace mine . --agent claude`. Mining is
idempotent — already-filed files are skipped.
Practical notes:
- `mempalace_search` is semantic — the `query` field takes **keywords only**, max 250
chars. Put background in `context`, not in the query, or the embedding gets diluted.
- Filter with `wing`/`room` when you know where the answer lives; the `sessions` wing is
raw transcript chunks and is noisy for factual lookups.
- **BM25 scores of 0 with mid-range similarity means no lexical match** — a bare-keyword
search for a proper noun that returns only semantic neighbours is a *negative* result,
not a weak positive. This is how the `odin` gap above was established.
- `mempalace_kg_query` hits the temporal knowledge graph for point-in-time facts; prefer
it over `search` for "what is X currently" questions, and use `kg_supersede` rather
than invalidate-then-add when a single-valued fact changes.
- Auto-save hooks are active: a **Stop** hook saves every 15 human messages, and a
**PreCompact** hook force-saves before context compaction.
Complementary and **separate** from MemPalace:
- **Per-project agent memory** — `~/.claude/projects/<slug>/memory/*.md`, indexed by that
directory's `MEMORY.md`. This repo's is currently empty; FamilySync's holds the CI-gate,
dev-stack, and Gitea-CI notes that much of this file's environment section derives from.
- **Project docs on disk** — `~/projects/familysync/CLAUDE.md` and `docs/ARCHITECTURE.md`
are the authoritative written source for the odin stack.
---
## Conventions
These apply to every collection unless its own README documents a deliberate exception.
### Shell
- `#!/usr/bin/env bash` and `set -euo pipefail` at the top of every bash entrypoint.
- Quote every expansion. Prefer `[[ ]]` over `[ ]`.
- Scripts must be **idempotent** — safe to re-run. Assume cron or a retry will do so.
- Accept `--dry-run` for anything that mutates state, deletes, or sends. Default to the
safe path when a flag is ambiguous.
- Log to stdout, errors to stderr. No log files unless the collection documents rotation.
### Secrets
- **Never commit secrets.** Read them from the environment or a file path passed in.
- Every collection that needs secrets ships a `.env.example` with dummy values and a
README line naming where the real values live.
- Note the FamilySync-side precedent: gitleaks runs as a blocking CI gate on that repo.
Assume any secret committed here is treated the same way — as a leak to be rotated, not
a mistake to be amended away.
### Portability
- Target the interpreters actually present: bash, Python 3, Node 22. Check before adding
a dependency on anything else.
- Unraid's userland is BusyBox-leaning in places — prefer POSIX-portable invocations of
`sed`/`awk`/`date` over GNU-only flags when the script runs on the server.
---
## Working agreements for Claude
- **Scope changes to one collection.** A request about collection X should not touch
collection Y. If it must, say so explicitly rather than doing it quietly.
- **Update the collection's README in the same change as the code.** Docs living beside
the script is the point of this repo's layout; a code change that leaves its README
stale defeats it.
- **Don't invent infrastructure.** If a script needs a host, path, port, or credential
that isn't documented above, ask — do not guess a plausible value. Wrong infra
assumptions in an ops script fail in production, not in review.
- **Destructive operations need confirmation** before they're run against odin, even when
the script itself is finished and correct. Writing the script is not authorization to
execute it.
+25
View File
@@ -0,0 +1,25 @@
# scripts
Operational scripts for my self-hosted environment (**odin**, an Unraid host running
Docker Compose behind Authelia and a Pangolin/Newt tunnel).
Each script collection lives in its own top-level folder and is fully self-contained —
its own README, config, and dependencies. Collections never import from each other.
See [CLAUDE.md](CLAUDE.md) for the environment details and repo conventions.
## Collections
_None yet._
<!-- Add one line per collection:
| [name](name/) | What it does | Runs on |
-->
## Adding a collection
```bash
cp -r _template <collection-name>
```
Fill in its `README.md`, then add a line to the table above.
+3
View File
@@ -0,0 +1,3 @@
# Copy to .env and fill in. Never commit the filled-in .env.
EXAMPLE_HOST=odin.bergerhouse.net
EXAMPLE_TOKEN=replace-me
+36
View File
@@ -0,0 +1,36 @@
# <collection-name>
One sentence: what this collection does and why it exists.
## Prerequisites
- Where it runs: `odin` (Unraid) / this workstation / either
- Interpreters: bash / Python 3 / Node 22
- External tools: e.g. `docker`, `tea`, `curl`
- Access needed: e.g. SSH to odin, Gitea API token
## Configuration
Copy `.env.example` to `.env` and fill it in. Real values live in `<where>`.
| Variable | Required | Description |
| --- | --- | --- |
| `EXAMPLE_HOST` | yes | Target host to operate against |
## Usage
```bash
bin/example --dry-run # preview, changes nothing
bin/example # apply
```
## Behaviour notes
- Idempotent: yes/no — and what happens on a re-run.
- Destructive operations: list them, or state "none".
- Scheduling: cron entry / Unraid User Scripts / manual only.
## Gotchas
Anything that cost time to discover. This is the section that earns the collection its
own folder — keep it honest and current.
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Template entrypoint. Copy, rename, and replace main().
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly COLLECTION_DIR="$(dirname "$SCRIPT_DIR")"
DRY_RUN=0
usage() {
cat <<'EOF'
Usage: example [--dry-run] [--help]
--dry-run Show what would happen without changing anything.
--help Show this message.
EOF
}
log() { printf '%s\n' "$*"; }
warn() { printf '%s\n' "$*" >&2; }
die() { warn "error: $*"; exit 1; }
# Run a mutating command, or describe it under --dry-run.
run() {
if (( DRY_RUN )); then
log "[dry-run] $*"
else
"$@"
fi
}
load_env() {
local env_file="$COLLECTION_DIR/.env"
[[ -f "$env_file" ]] || return 0
set -a; . "$env_file"; set +a
}
main() {
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--help|-h) usage; exit 0 ;;
*) die "unknown argument: $1" ;;
esac
shift
done
load_env
: "${EXAMPLE_HOST:?EXAMPLE_HOST is not set — see .env.example}"
log "target: $EXAMPLE_HOST"
run true # replace with the real work
}
main "$@"
+28
View File
@@ -0,0 +1,28 @@
# Wing is set explicitly, not auto-detected from the directory basename —
# "scripts" is a common folder name and would collide with other corpora.
wing: odin-scripts
rooms:
- name: environment
description: Repo-wide context — the odin/Unraid environment, conventions, agent guidance
keywords:
- environment
- odin
- unraid
- conventions
- CLAUDE
- name: documentation
description: Per-collection READMEs and docs/ content
keywords:
- documentation
- docs
- readme
# NB: do not use "scripts" as a keyword here — it matches the repo's own path
# (/home/luc/projects/scripts) and would swallow every file into this room.
- name: collections
description: Executable entrypoints and sourced helpers (bin/, lib/)
keywords:
- bin
- lib
- name: general
description: Files that don't fit other rooms
keywords: []