diff --git a/_primitives/kei-sleep-setup.sh b/_primitives/kei-sleep-setup.sh new file mode 100755 index 0000000..350c6fa --- /dev/null +++ b/_primitives/kei-sleep-setup.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# kei-sleep-setup.sh — KeiSeiKit v0.11 sleep-sync first-time wizard. +# Generates deploy key, scaffolds sync-repo, writes env refs (RULE 0.8). +# Idempotent. Invoked by `/sleep-setup` skill or `install.sh --with-sleep-sync`. +set -eu + +KEI_HOME="${HOME}/.claude" +SECRETS_FILE="${KEI_HOME}/secrets/.env" +SSH_KEY="${HOME}/.ssh/keisei-memory-sync" +REPO_PATH="${KEI_HOME}/memory/sync-repo" + +say() { printf '[sleep-setup] %s\n' "$*"; } +warn() { printf '[sleep-setup] warn: %s\n' "$*" >&2; } +err() { printf '[sleep-setup] error: %s\n' "$*" >&2; } + +validate_ssh_url() { + printf '%s' "$1" | grep -Eq '^git@[A-Za-z0-9._-]+:[A-Za-z0-9._/-]+\.git$' +} + +url_host() { + printf '%s' "$1" | sed -E 's/^git@([^:]+):.*/\1/' +} + +prompt_repo_url() { + local url="${KEI_MEMORY_REPO_URL:-}" + if [ -n "$url" ]; then + say "using KEI_MEMORY_REPO_URL from environment" + elif [ -t 0 ]; then + printf '\nMemory repo SSH URL (e.g. git@github.com:you/kei-memory.git):\n > ' + read -r url + else + err "no repo URL provided and no TTY to prompt; set KEI_MEMORY_REPO_URL" + exit 1 + fi + if ! validate_ssh_url "$url"; then + err "invalid SSH URL format: $url" + err "expected: git@:/.git" + exit 1 + fi + printf '%s' "$url" +} + +ensure_ssh_key() { + mkdir -p "$(dirname "$SSH_KEY")" + chmod 700 "$(dirname "$SSH_KEY")" 2>/dev/null || true + if [ -f "$SSH_KEY" ] && [ -f "${SSH_KEY}.pub" ]; then + say "deploy key already exists at $SSH_KEY (skipping)" + return 0 + fi + say "generating ed25519 deploy key at $SSH_KEY" + ssh-keygen -t ed25519 -f "$SSH_KEY" -N '' -C 'keisei-memory-sync' >/dev/null + chmod 600 "$SSH_KEY" +} + +show_deploy_key_instructions() { + local url="$1" + printf '\n==============================================================\n' + printf ' ADD THIS AS A DEPLOY KEY (WRITE ACCESS) TO: %s\n' "$url" + printf '==============================================================\n\n' + cat "${SSH_KEY}.pub" + printf '\nFingerprint: ' + ssh-keygen -lf "${SSH_KEY}.pub" 2>/dev/null || true + printf '\nGitHub: Settings -> Deploy keys -> Add ("Allow write access")\n' + printf 'GitLab: Settings -> Repository -> Deploy keys -> Enable write\n' + printf 'Bitbucket: Repository settings -> Access keys -> Add (write)\n' + printf 'Forgejo: Settings -> Deploy Keys -> Add (allow write)\n' + printf '==============================================================\n\n' +} + +init_sync_repo() { + local url="$1" + mkdir -p "$REPO_PATH" + if [ -d "${REPO_PATH}/.git" ]; then + say "sync-repo already initialized at $REPO_PATH" + return 0 + fi + say "cloning $url → $REPO_PATH (shallow, may fail if repo empty — will init instead)" + if GIT_SSH_COMMAND="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new" \ + git clone --depth 1 "$url" "$REPO_PATH" 2>/dev/null; then + say "cloned existing repo" + else + say "clone failed (empty repo?) — initializing local and setting remote" + rm -rf "$REPO_PATH" + mkdir -p "$REPO_PATH" + ( cd "$REPO_PATH" && git init -q -b main && git remote add origin "$url" ) + fi +} + +scaffold_repo_structure() { + local url="$1" + cd "$REPO_PATH" + mkdir -p traces reports + [ -f README.md ] || write_readme + [ -f .gitignore ] || printf 'target/\nnode_modules/\n.DS_Store\n*.swp\n*.swo\n' > .gitignore + [ -f backlog.md ] || write_backlog + [ -f .keisei-sync.toml ] || write_sync_config "$url" +} + +write_readme() { + cat > README.md <<'EOF' +# KeiSeiKit memory store + +Append-only store for KeiSeiKit session traces + nightly REM reports. +Managed by kei-sleep-sync; do not hand-edit. + +- traces/ — session JSONL pushed after each Claude Code session +- reports/ — nightly reports written by a cloud agent on /schedule +- backlog.md — recurring patterns flagged for your review +EOF +} + +write_backlog() { + cat > backlog.md <<'EOF' +# REM backlog — recurring patterns + +Nightly consolidation prepends dated blocks when >=3 patterns recur. +Pop entries manually after review. + + +EOF +} + +write_sync_config() { + cat > .keisei-sync.toml </dev/null || true + touch "$SECRETS_FILE" + chmod 600 "$SECRETS_FILE" + # Remove any prior KEI_MEMORY_* lines (idempotent update). + local tmp + tmp="$(mktemp)" + grep -vE '^(KEI_MEMORY_REPO_URL|KEI_MEMORY_REPO_PATH|KEI_MEMORY_SSH_KEY)=' \ + "$SECRETS_FILE" > "$tmp" 2>/dev/null || true + cat >> "$tmp" <&1 || true)" + if printf '%s' "$out" | grep -Eiq '(successfully authenticated|welcome|you.?ve|does not provide shell access)'; then + say "SSH auth OK ($host)" + return 0 + fi + warn "SSH auth to $host did not return a known success banner" + warn "server said: $(printf '%s' "$out" | head -n1)" + warn "if the deploy key was just added it may need 30-60s to propagate" + return 1 +} + +main() { + say "KeiSeiKit v0.11 sleep-sync setup" + local url + url="$(prompt_repo_url)" + ensure_ssh_key + show_deploy_key_instructions "$url" + if [ -t 0 ]; then + printf 'Deploy key added to the repo? Press ENTER to continue, Ctrl-C to abort.\n' + read -r _ || true + fi + init_sync_repo "$url" + scaffold_repo_structure "$url" + write_env_refs "$url" + test_ssh_auth "$url" || warn "continuing despite auth test warning" + echo + say "setup complete" + say "next: run /sleep-setup in Claude Code to register a nightly /schedule trigger" +} + +main "$@" diff --git a/_primitives/kei-sleep-sync.sh b/_primitives/kei-sleep-sync.sh new file mode 100755 index 0000000..25de59e --- /dev/null +++ b/_primitives/kei-sleep-sync.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# kei-sleep-sync.sh — POSIX-sh helper called at session end. +# +# Stages any new session traces + backlog in the user's memory-repo and +# pushes via a dedicated deploy key. NEVER blocks the session: every +# failure path logs to ~/.claude/memory/sync-errors.log and exits 0. +# +# Config resolution order: +# 1. env var KEI_MEMORY_REPO_PATH / KEI_MEMORY_SSH_KEY +# 2. ~/.claude/secrets/.env (sourced if present) +# 3. sync-repo's .keisei-sync.toml (informational only) +# +# Emergency bypass: `KEI_SLEEP_SYNC_BYPASS=1 ...` — silent exit 0. + +set -u + +ERR_LOG="${HOME}/.claude/memory/sync-errors.log" + +log_err() { + mkdir -p "$(dirname "$ERR_LOG")" 2>/dev/null || return 0 + printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$ERR_LOG" 2>/dev/null || true +} + +# ---- bypass + env ----------------------------------------------------------- + +[ "${KEI_SLEEP_SYNC_BYPASS:-0}" = "1" ] && exit 0 + +SECRETS_FILE="${HOME}/.claude/secrets/.env" +if [ -f "$SECRETS_FILE" ] && [ -z "${KEI_MEMORY_REPO_PATH:-}" ]; then + # shellcheck disable=SC1090 + . "$SECRETS_FILE" 2>/dev/null || true +fi + +REPO_PATH="${KEI_MEMORY_REPO_PATH:-}" +SSH_KEY="${KEI_MEMORY_SSH_KEY:-}" + +# Silent no-op when sync isn't configured yet (most users). +[ -z "$REPO_PATH" ] && exit 0 +[ -d "${REPO_PATH}/.git" ] || exit 0 + +# ---- stage, commit, push --------------------------------------------------- + +# cd may fail (permissions / path vanished) — silent exit. +cd "$REPO_PATH" 2>/dev/null || exit 0 + +# Mirror traces from the canonical local dump dir into the repo. +TRACES_SRC="${HOME}/.claude/memory/traces" +if [ -d "$TRACES_SRC" ]; then + mkdir -p traces 2>/dev/null || true + # -n = never overwrite; append-only semantics. + cp -n "$TRACES_SRC"/*.jsonl traces/ 2>/dev/null || true +fi + +git add traces/ backlog.md 2>/dev/null || { log_err "git add failed"; exit 0; } + +# Nothing staged — silent exit. +if git diff --cached --quiet 2>/dev/null; then + exit 0 +fi + +COMMIT_MSG="memory: session traces $(date +%Y-%m-%dT%H:%M:%S%z)" +if ! git commit -q -m "$COMMIT_MSG" 2>/dev/null; then + log_err "git commit failed" + exit 0 +fi + +# Push via the dedicated deploy key so we don't clobber the user's default SSH. +if [ -n "$SSH_KEY" ] && [ -f "$SSH_KEY" ]; then + GIT_SSH_COMMAND="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new" \ + git push -q origin HEAD 2>/dev/null \ + || { log_err "git push failed via $SSH_KEY"; exit 0; } +else + git push -q origin HEAD 2>/dev/null \ + || { log_err "git push failed (no SSH_KEY set)"; exit 0; } +fi + +exit 0 diff --git a/_primitives/templates/sleep-trigger-prompt.md b/_primitives/templates/sleep-trigger-prompt.md new file mode 100644 index 0000000..d8bf0e3 --- /dev/null +++ b/_primitives/templates/sleep-trigger-prompt.md @@ -0,0 +1,94 @@ +# Nightly REM consolidation (KeiSeiKit v0.11 sleep-sync) + + + +Clone: {REPO_URL} +Branch: main +Time: 03:00 local (UTC cron: {UTC_CRON}) + +## Task + +1. Clone the memory repo shallow. +2. Identify NEW traces in `traces/` since the last consolidation by + comparing filenames against `reports/last-run.txt` (if the file is + missing, treat ALL traces as new on the first run). +3. For each new trace (JSONL, one event per line), extract: + - user prompts (role = "user", type = "message") + - tool calls (type = "tool_use", name + input summary) + - tool errors (is_error = true) + - session duration (first vs last timestamp) +4. Group events into topics via simple keyword matching on user prompts + (no ML, no embeddings — keyword co-occurrence ≥ 2 is enough). +5. Count recurring patterns: any tool-call sequence OR error class that + appears in ≥ 2 distinct sessions is a "cross-session pattern". +6. Write `reports/sleep-YYYY-MM-DD.md` with this structure: + + ``` + # REM report — YYYY-MM-DD + + Sessions analyzed: + Total duration: + + ## Top tool-call sequences (cross-session) + 1. × + ... + + ## Top error classes + 1. × + ... + + ## Suggested rule/hook candidates (dry-run only) + - [ ] — why () + ... + ``` + +7. If there are ≥ 3 cross-session patterns, prepend a timestamped block + to `backlog.md`: + + ``` + ## YYYY-MM-DD — REM consolidation + - + - + - + ``` + +8. Write a single line to `reports/last-run.txt` with this run's + ISO-8601 UTC timestamp (overwrite, no append). +9. Stage, commit, push: + + ``` + git add reports/ backlog.md + git commit -m "REM: consolidation $(date -I)" + git push + ``` + +## Invariants + +- Traces are append-only. Never delete or modify `traces/*.jsonl`. +- If nothing recurred this cycle, the report MUST still be written — + with body "no patterns this cycle" — so you can tell "ran and found + nothing" apart from "did not run". +- Never fabricate findings. If the analyzer outputs an empty list, + emit an empty report. +- Never paraphrase patent-sensitive content from the traces into the + report body. Install a project-local pre-commit gate on the + memory-repo if you want hard enforcement of that boundary. +- Success signal = commit pushed cleanly. Anything else is a failure + that surfaces to the user on the next `git pull`. + +## Failure handling + +- Clone fails → post an issue to the repo if possible; otherwise exit 1. +- Commit hook blocks → do NOT force-push. Write the failure reason to + `reports/sleep-YYYY-MM-DD.md` body and attempt a commit excluding the + offending file. +- Push fails → retry once with exponential backoff; on second failure, + leave local commit in place and exit 1 (next run will push).