From 0483a2de2cdd362a82ae57ecfccfa9e94dee46a7 Mon Sep 17 00:00:00 2001 From: Ori Nachum Date: Wed, 24 Jun 2026 08:33:50 +0300 Subject: [PATCH] eidetic-memory: ### Added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Memory-discipline "Conventions and workflow" section in `CLAUDE.md`** — a per-task *recall-before / remember-after* convention (scope localized to this repo's nick) so the vendored `remember` / `recall` skills are actually used, not just present: `/recall` before non-trivial work to build on prior decisions instead of re-deriving them, and `/remember` when a non-obvious decision, constraint, fix-and-why, or hard-won gotcha surfaces. The section documents this repo's memory as **in-repo and public** — records resolve to `/.eidetic/memory` (committed, team- and mesh-shared). Inserted idempotently (skipped if already present), slotted under an existing "Conventions and workflow" heading when one exists, else appended. ### Changed - **Refreshed the `remember` + `recall` wrappers from eidetic-cli 0.10.0** (cite-don't-import) — picks up eidetic's **project-local store default**: the files backend now resolves per record by visibility — PUBLIC records inside a git repo go to `/.eidetic/memory` (committed, team-shared), PRIVATE records (or any record outside a repo) go to `$HOME/.eidetic/memory` (never committed), an explicit `EIDETIC_DATA_DIR` still wins, and recall reads both stores and merges. Also carries the 0.9.3 hardening (interactive-stdin guard, `help` as a search term, SIGPIPE-safe suffix parsing). **Recipe policy override (the wrappers here are NOT byte-verbatim):** the injected default visibility is flipped from eidetic's `private` to **`public`**, so a plain `/remember` lands the note in `./.eidetic/memory` in this repo, kept as part of the repo — pass `--visibility private` to route a record to `$HOME` instead. `remember` drives `eidetic remember` (idempotent upsert of one JSON record or an NDJSON batch on stdin); `recall` drives `eidetic recall` with four search modes (exact / approximate / keyword / hybrid). Each `SKILL.md` is localized only in the illustrative `--scope ` examples (Provenance keeps "First-party to eidetic-cli"). Runtime dep: the `eidetic` CLI on PATH (else a local eidetic-cli checkout with `uv`) — **`eidetic >= 0.10.0`** for the in-repo routing; on an older CLI the public records still work but are stored in `$HOME/.eidetic/memory` instead of in-repo. Propagated by rollout-cli's `eidetic-memory` recipe. --- .claude/skills/recall/scripts/recall.sh | 48 +++++++++++++------ .claude/skills/remember/scripts/remember.sh | 52 +++++++++++++++------ CHANGELOG.md | 39 ++++++++++++++++ CLAUDE.md | 28 +++++++++++ pyproject.toml | 2 +- 5 files changed, 142 insertions(+), 27 deletions(-) diff --git a/.claude/skills/recall/scripts/recall.sh b/.claude/skills/recall/scripts/recall.sh index 9fe9741..6561b63 100755 --- a/.claude/skills/recall/scripts/recall.sh +++ b/.claude/skills/recall/scripts/recall.sh @@ -6,10 +6,11 @@ # forwards every flag verbatim — so `recall.sh "" --mode hybrid --json` # is exactly `eidetic recall "" --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. +# The store is the files backend. Default location resolves per-operation: +# PUBLIC records inside a git repo → /.eidetic/memory (committed, +# team-shared); PRIVATE records, or any record outside a git repo → +# $HOME/.eidetic/memory (never committed). Recall reads both stores and merges. +# An explicit EIDETIC_DATA_DIR wins and short-circuits to that single dir. set -euo pipefail @@ -34,12 +35,10 @@ resolve_eidetic() { 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 + # In a vendored copy there is no eidetic-cli checkout to fall back to, so the + # only honest remedy is to install the CLI. One `error:` + one `hint:` line. + printf 'error: eidetic CLI not found.\n' >&2 + printf 'hint: install it with: uv tool install eidetic-cli (or pipx install eidetic-cli); the console script is eidetic.\n' >&2 return 1 } @@ -65,10 +64,17 @@ EOF } case "${1:-}" in - -h | --help | help | "") + -h | --help) usage exit 0 ;; + "") + # A missing query is a usage error, not success. The bareword `help` is + # a legitimate search term, so it is intentionally NOT a usage alias. + printf 'error: no query given.\n' >&2 + printf 'hint: recall.sh "" [--mode ...] [--json]; run recall.sh --help for usage.\n' >&2 + exit 1 + ;; esac resolve_eidetic || exit 2 @@ -100,9 +106,12 @@ resolve_scope() { # 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. + # `|| true`: under `set -o pipefail`, `head -n1` closing the pipe + # early can SIGPIPE `sed`, making the substitution non-zero and + # aborting the script. An empty parse must yield "" here, not exit. suffix=$(sed -n \ 's/^[[:space:]]*-\{0,1\}[[:space:]]*suffix:[[:space:]]*\([^[:space:]]*\).*/\1/p' \ - "$dir/culture.yaml" | head -n1 | tr -d "\"'") + "$dir/culture.yaml" | head -n1 | tr -d "\"'" || true) break fi dir=$(dirname "$dir") @@ -127,7 +136,20 @@ 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) + # rollout-cli eidetic-memory recipe POLICY OVERRIDE (not eidetic's + # upstream private default): default to PUBLIC, so a plain recall queries + # the in-repo public pool (/.eidetic/memory) this repo writes to. + # Pass --visibility private to also surface this agent's private ($HOME) + # notes. The two-store read model reads both dirs regardless. + has_flag --visibility "$@" || SCOPE_ARGS+=(--visibility public) + elif ! has_flag --visibility "$@"; then + # No suffix AND no explicit --visibility: the query runs against + # eidetic's own default (scope=default, visibility=public), not this + # agent's private personal scope — so an empty result isn't silently + # misread. Warn on stderr (stdout stays clean for --json). Warn ONLY + # here: an explicit --scope (outer guard) or --visibility (this guard) is + # a deliberate choice, honored verbatim, so either flag silences this. + printf 'warning: no culture.yaml suffix resolved; querying the public default scope rather than a private personal scope. Pass --scope or --visibility to target deliberately.\n' >&2 fi fi diff --git a/.claude/skills/remember/scripts/remember.sh b/.claude/skills/remember/scripts/remember.sh index 164736e..e4b6908 100755 --- a/.claude/skills/remember/scripts/remember.sh +++ b/.claude/skills/remember/scripts/remember.sh @@ -12,10 +12,11 @@ # Upsert is idempotent by id (and dedups by content hash): re-remembering the # same record updates it in place, never duplicates. # -# The store is the files backend at ~/.eidetic/memory by default — a home-dir -# path OUTSIDE any git worktree, so a record Claude remembers is recallable by -# the colleague backend (which runs in throwaway worktrees), and vice versa. -# Set EIDETIC_DATA_DIR to opt out of sharing; use --backend mongo|neo4j (with +# The store is the files backend. Default location resolves per-operation: +# PUBLIC records inside a git repo → /.eidetic/memory (committed, +# team-shared); PRIVATE records, or any record outside a git repo → +# $HOME/.eidetic/memory (never committed). An explicit EIDETIC_DATA_DIR still +# wins and short-circuits to that single dir. Use --backend mongo|neo4j (with # EIDETIC_MONGO_URI / NEO4J_URI) for a server-backed shared store. set -euo pipefail @@ -40,12 +41,10 @@ resolve_eidetic() { 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 + # In a vendored copy there is no eidetic-cli checkout to fall back to, so the + # only honest remedy is to install the CLI. One `error:` + one `hint:` line. + printf 'error: eidetic CLI not found.\n' >&2 + printf 'hint: install it with: uv tool install eidetic-cli (or pipx install eidetic-cli); the console script is eidetic.\n' >&2 return 1 } @@ -60,7 +59,9 @@ Usage: A record needs `id`, `text`, and `type`; `hash` and `metadata` are recommended (hash is derived from text when omitted). Upsert is idempotent by id. -Public data only. Every flag is forwarded verbatim to `eidetic remember`. +Records default to this agent's PRIVATE personal scope (--scope from the +culture.yaml suffix); pass --visibility public to contribute to the shared +public pool. Every flag is forwarded verbatim to `eidetic remember`. See `eidetic explain remember`. EOF } @@ -72,6 +73,16 @@ case "${1:-}" in ;; esac +# No record argument AND stdin is an interactive terminal → `eidetic remember` +# would block forever waiting for NDJSON. Show usage instead of hanging. A piped +# or redirected stdin (`cat records.ndjson | remember.sh`) is not a TTY and +# proceeds to the batch path normally. +if [ "$#" -eq 0 ] && [ -t 0 ]; then + usage >&2 + printf 'hint: pass a JSON record as an argument, or pipe NDJSON on stdin.\n' >&2 + exit 1 +fi + resolve_eidetic || exit 2 # ── default to this agent's PERSONAL, PRIVATE scope (culture.yaml `suffix`) ── @@ -100,9 +111,12 @@ resolve_scope() { # 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. + # `|| true`: under `set -o pipefail`, `head -n1` closing the pipe + # early can SIGPIPE `sed`, making the substitution non-zero and + # aborting the script. An empty parse must yield "" here, not exit. suffix=$(sed -n \ 's/^[[:space:]]*-\{0,1\}[[:space:]]*suffix:[[:space:]]*\([^[:space:]]*\).*/\1/p' \ - "$dir/culture.yaml" | head -n1 | tr -d "\"'") + "$dir/culture.yaml" | head -n1 | tr -d "\"'" || true) break fi dir=$(dirname "$dir") @@ -127,7 +141,19 @@ 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) + # rollout-cli eidetic-memory recipe POLICY OVERRIDE (not eidetic's + # upstream private default): default to PUBLIC so a plain remember lands + # in /.eidetic/memory — committed, team- and mesh-shared. Pass + # --visibility private to keep a record in $HOME (uncommitted). + has_flag --visibility "$@" || SCOPE_ARGS+=(--visibility public) + elif ! has_flag --visibility "$@"; then + # No suffix AND no explicit --visibility: the record falls back to + # eidetic's own default (scope=default, visibility=public). Don't let an + # expected-private record go public silently — warn on stderr (stdout + # stays clean for --json). Warn ONLY here: an explicit --scope (outer + # guard) or --visibility (this guard) means the caller chose deliberately + # and is honored verbatim, so either flag silences this. + printf 'warning: no culture.yaml suffix resolved; this record falls back to the public default scope. Pass --scope or --visibility to place it deliberately.\n' >&2 fi fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 25bd31f..6d49bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - 2026-06-24 + +### Added + +- **Memory-discipline "Conventions and workflow" section in `CLAUDE.md`** — a + per-task *recall-before / remember-after* convention (scope localized to this + repo's nick) so the vendored `remember` / `recall` skills are actually used, + not just present: `/recall` before non-trivial work to build on prior + decisions instead of re-deriving them, and `/remember` when a non-obvious + decision, constraint, fix-and-why, or hard-won gotcha surfaces. The section + documents this repo's memory as **in-repo and public** — records resolve to + `/.eidetic/memory` (committed, team- and mesh-shared). Inserted + idempotently (skipped if already present), slotted under an existing + "Conventions and workflow" heading when one exists, else appended. + +### Changed + +- **Refreshed the `remember` + `recall` wrappers from eidetic-cli 0.10.0** + (cite-don't-import) — picks up eidetic's **project-local store default**: the + files backend now resolves per record by visibility — PUBLIC records inside a + git repo go to `/.eidetic/memory` (committed, team-shared), PRIVATE + records (or any record outside a repo) go to `$HOME/.eidetic/memory` (never + committed), an explicit `EIDETIC_DATA_DIR` still wins, and recall reads both + stores and merges. Also carries the 0.9.3 hardening (interactive-stdin guard, + `help` as a search term, SIGPIPE-safe suffix parsing). **Recipe policy + override (the wrappers here are NOT byte-verbatim):** the injected default + visibility is flipped from eidetic's `private` to **`public`**, so a plain + `/remember` lands the note in `./.eidetic/memory` in this repo, kept as part + of the repo — pass `--visibility private` to route a record to `$HOME` + instead. `remember` drives `eidetic remember` (idempotent upsert of one JSON + record or an NDJSON batch on stdin); `recall` drives `eidetic recall` with + four search modes (exact / approximate / keyword / hybrid). Each `SKILL.md` is + localized only in the illustrative `--scope ` examples (Provenance keeps + "First-party to eidetic-cli"). Runtime dep: the `eidetic` CLI on PATH (else a + local eidetic-cli checkout with `uv`) — **`eidetic >= 0.10.0`** for the + in-repo routing; on an older CLI the public records still work but are stored + in `$HOME/.eidetic/memory` instead of in-repo. Propagated by rollout-cli's + `eidetic-memory` recipe. + ## [0.3.0] - 2026-06-23 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index ebf66cb..fc18765 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,3 +26,31 @@ Until you run `/init`, `refactor-cli` satisfies the `steward doctor` `prompt-file-present` and `backend-consistency` invariants (a `CLAUDE.md` exists and `culture.yaml` declares `backend: claude`) but the prompt is not yet tailored to this agent's domain. + +## Conventions and workflow + +**Memory discipline — recall before, remember after.** This repo keeps its +eidetic memory **in-repo and public**: records resolve to +`/.eidetic/memory` — committed, and shared with the team and mesh +peers (the `claude` and `colleague` backends both read the same +`refactor-cli` scope), so memory travels with the repo, not a private +home-dir store. Make it a per-task habit: + +- **`/recall` before you start.** Search the store for the area you're about + to touch — prior decisions, gotchas, "have we done this before?" — so you + build on what's already known instead of re-deriving it. Do this before + non-trivial tasks, not just when asked. +- **`/remember` when something worth keeping surfaces.** A non-obvious + decision and its rationale, a constraint, a fix and *why* it was needed, a + gotcha that cost time, a fact the next session would otherwise re-learn. + Capture it as it happens, not at the end when it's faded. + +A plain `/remember` lands the note in `./.eidetic/memory` in this repo — no +flag needed (the wrappers here default to `--visibility public`; in-repo +routing needs `eidetic >= 0.10.0`, older CLIs keep records in `$HOME`). Keep +something out of the committed store only by passing `--visibility private` +(routes to `$HOME/.eidetic/memory`, never committed); `/recall` reads both +stores and merges. Don't store what the repo already records (code structure, +git history, what's already in this file or `CHANGELOG.md`) — store what you'd +have to re-derive. These are the `recall`/`remember` skills (`.claude/skills/`), +backed by the `eidetic` store. diff --git a/pyproject.toml b/pyproject.toml index 1adb5b9..dc45c4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "refactor-cli" -version = "0.3.0" +version = "0.4.0" description = "Atomic in-repo transformation engine — composable, behavior-preserving refactor actions that don't change what the code does." readme = "README.md" license = "MIT"