feat(primitives): kei-sleep-setup wizard + kei-sleep-sync helper + trigger template
This commit is contained in:
parent
48b1a8cdcf
commit
9450ef0b95
3 changed files with 363 additions and 0 deletions
192
_primitives/kei-sleep-setup.sh
Executable file
192
_primitives/kei-sleep-setup.sh
Executable 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
77
_primitives/kei-sleep-sync.sh
Executable 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
|
||||
94
_primitives/templates/sleep-trigger-prompt.md
Normal file
94
_primitives/templates/sleep-trigger-prompt.md
Normal 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).
|
||||
Loading…
Reference in a new issue