feat(primitives): kei-sleep-setup wizard + kei-sleep-sync helper + trigger template

This commit is contained in:
Parfii-bot 2026-04-22 01:34:42 +08:00
parent 48b1a8cdcf
commit 9450ef0b95
3 changed files with 363 additions and 0 deletions

192
_primitives/kei-sleep-setup.sh Executable file
View file

@ -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@<host>:<org>/<repo>.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.
<!-- populated by the cloud agent -->
EOF
}
write_sync_config() {
cat > .keisei-sync.toml <<EOF
# KeiSeiKit sleep-sync config (per-repo)
repo_url = "$1"
push_on_session_end = true
branch = "main"
commit_prefix = "memory"
EOF
}
write_env_refs() {
local url="$1"
mkdir -p "$(dirname "$SECRETS_FILE")"
chmod 700 "$(dirname "$SECRETS_FILE")" 2>/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" <<EOF
KEI_MEMORY_REPO_URL=$url
KEI_MEMORY_REPO_PATH=$REPO_PATH
KEI_MEMORY_SSH_KEY=$SSH_KEY
EOF
mv "$tmp" "$SECRETS_FILE"
chmod 600 "$SECRETS_FILE"
say "wrote env refs to $SECRETS_FILE"
}
test_ssh_auth() {
local url="$1"
local host
host="$(url_host "$url")"
say "testing SSH auth to $host"
# ssh -T on git hosts returns non-zero even on success; grep the banner.
local out
out="$(ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new \
-o BatchMode=yes -T "git@$host" 2>&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 "$@"

77
_primitives/kei-sleep-sync.sh Executable file
View file

@ -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

View file

@ -0,0 +1,94 @@
# Nightly REM consolidation (KeiSeiKit v0.11 sleep-sync)
<!--
Template prompt. Render placeholders before pasting into Claude Code
`/schedule create`. The sleep-setup skill does this for you; this file
exists so power-users can customise the prompt before scheduling.
Placeholders:
{REPO_URL} — your memory-repo SSH URL (git@host:org/repo.git)
{UTC_CRON} — cron expression in UTC (sleep-setup converts local 03:00)
-->
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: <count>
Total duration: <hh:mm>
## Top tool-call sequences (cross-session)
1. <seq> ×<count>
...
## Top error classes
1. <class> ×<count>
...
## Suggested rule/hook candidates (dry-run only)
- [ ] <name> — why (<E-grade>)
...
```
7. If there are ≥ 3 cross-session patterns, prepend a timestamped block
to `backlog.md`:
```
## YYYY-MM-DD — REM consolidation
- <pattern 1>
- <pattern 2>
- <pattern 3>
```
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).