Bryan's pivot: until bjnoela.com is back online, transfer learnings via
local file capture on the client + manual paste-back to home-Larry. NO
credentials required on the client box.
Capture flow:
- lib/lessons.sh records lessons to $LARRY_HOME/lessons/<date>.md
- lesson_record tool in larry.sh lets the agent record proactively
- /lesson, /lessons, /export REPL commands
- agents/larry.md updated: capture corrections, conventions, quirks
silently when Bryan teaches them
Export flow:
- lessons.sh export | bundle | --gh-issue (uses gh CLI if available)
- Bryan pastes the bundle to home-Larry on his dev machine
- home-Larry commits the refinement into cloverleaf-larry/agents/
- next launch on any client pulls updated persona via self-update
Brings total native tools to 28.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
221 lines
7.8 KiB
Bash
Executable File
221 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# lessons.sh — local-first lesson capture for Larry-Anywhere.
|
|
#
|
|
# When Bryan teaches Larry something during a session — a correction, a new
|
|
# pattern, a gotcha, a site-specific quirk — it gets appended here. Lessons
|
|
# stay LOCAL by default; they never auto-leave the client box. When Bryan can
|
|
# reach his dev machine again, he runs `lessons.sh export` and copies the
|
|
# bundle back to me (the home Larry) so I can commit it into the canonical
|
|
# agents/ persona files in the cloverleaf-larry repo.
|
|
#
|
|
# Storage: $LARRY_HOME/lessons/<YYYY-MM-DD>.md (one file per day, append-only).
|
|
# Plus a journal at $LARRY_HOME/lessons/_index.tsv for machine parsing.
|
|
#
|
|
# Subcommands:
|
|
# add "text" [--topic X] [--site Y] [--severity info|warn|fix]
|
|
# append a lesson
|
|
# list newest first, with index ids
|
|
# show <id> one lesson
|
|
# export [--since YYYY-MM-DD] one big markdown bundle to stdout
|
|
# (paste this back to me on your dev machine)
|
|
# export --bundle PATH write the bundle to a file
|
|
# export --gh-issue format as a GitHub Issue body + open with `gh` if available
|
|
# clear [--before YYYY-MM-DD] delete uploaded lessons (use after a successful upload)
|
|
# count just the number of unbundled lessons
|
|
set -o pipefail
|
|
|
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
|
LESSONS_DIR="$LARRY_HOME/lessons"
|
|
LESSONS_INDEX="$LESSONS_DIR/_index.tsv"
|
|
|
|
mkdir -p "$LESSONS_DIR" 2>/dev/null
|
|
chmod 700 "$LESSONS_DIR" 2>/dev/null || true
|
|
|
|
if [ ! -f "$LESSONS_INDEX" ]; then
|
|
printf 'timestamp\tid\ttopic\tsite\tseverity\tfile\n' > "$LESSONS_INDEX"
|
|
fi
|
|
|
|
die() { printf 'lessons: %s\n' "$*" >&2; exit 1; }
|
|
|
|
_next_id() {
|
|
local n
|
|
n=$(awk -F'\t' 'NR>1{print $2}' "$LESSONS_INDEX" | sort -n | tail -1)
|
|
printf '%04d' $(( ${n:-0} + 1 ))
|
|
}
|
|
|
|
cmd_add() {
|
|
local text="$1"; shift
|
|
local topic="" site="" severity="info"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--topic) shift; topic="$1" ;;
|
|
--site) shift; site="$1" ;;
|
|
--severity) shift; severity="$1" ;;
|
|
*) die "unknown flag: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
[ -n "$text" ] || die "lesson text is required"
|
|
|
|
local id day file ts
|
|
id=$(_next_id)
|
|
day=$(date +%Y-%m-%d)
|
|
ts=$(date -Iseconds 2>/dev/null || date)
|
|
file="$LESSONS_DIR/${day}.md"
|
|
|
|
if [ ! -f "$file" ]; then
|
|
{
|
|
printf '# Lessons captured %s\n\n' "$day"
|
|
printf '_From Larry-Anywhere on %s. Append-only._\n\n' "$(hostname 2>/dev/null || echo unknown)"
|
|
} > "$file"
|
|
fi
|
|
|
|
{
|
|
printf '\n## %s — %s' "$id" "$ts"
|
|
[ -n "$topic" ] && printf ' · **%s**' "$topic"
|
|
[ -n "$site" ] && printf ' · site=`%s`' "$site"
|
|
[ "$severity" != "info" ] && printf ' · severity=%s' "$severity"
|
|
[ -n "${HCIROOT:-}" ] && printf ' · HCIROOT=`%s`' "$HCIROOT"
|
|
[ -n "${HCISITE:-}" ] && printf ' · HCISITE=`%s`' "$HCISITE"
|
|
printf '\n\n%s\n' "$text"
|
|
} >> "$file"
|
|
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$ts" "$id" "${topic:-}" "${site:-${HCISITE:-}}" "$severity" "$file" >> "$LESSONS_INDEX"
|
|
|
|
printf 'lesson %s captured → %s\n' "$id" "$file"
|
|
}
|
|
|
|
cmd_list() {
|
|
local n; n=$(($(wc -l < "$LESSONS_INDEX") - 1))
|
|
printf 'lessons: %d captured (newest first)\n\n' "$n"
|
|
printf ' id timestamp topic site file\n'
|
|
awk -F'\t' 'NR>1 { lines[++i] = sprintf(" %s %-26s %-22s %-14s %s", $2, $1, ($3=="" ? "—" : $3), ($4=="" ? "—" : $4), $6) }
|
|
END { for (k=i; k>=1; k--) print lines[k] }' "$LESSONS_INDEX"
|
|
}
|
|
|
|
cmd_show() {
|
|
local id="$1"
|
|
[ -n "$id" ] || die "usage: lessons.sh show <id>"
|
|
local file ts topic
|
|
awk -F'\t' -v id="$id" 'NR>1 && $2==id { print; exit }' "$LESSONS_INDEX" \
|
|
| { IFS=$'\t' read -r ts iid topic site sev file
|
|
[ -z "$file" ] && { die "no such lesson id: $id"; }
|
|
printf '## %s (%s)\n topic: %s · site: %s · severity: %s · file: %s\n\n' \
|
|
"$id" "$ts" "${topic:-—}" "${site:-—}" "$sev" "$file"
|
|
awk -v marker="## $id" '
|
|
$0 ~ marker, /^## [0-9]{4} —/ { if (NR>1 && /^## [0-9]{4} —/ && $0 !~ marker) exit; print }
|
|
' "$file"
|
|
}
|
|
}
|
|
|
|
cmd_export() {
|
|
local since="" out="" mode="stdout"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--since) shift; since="$1" ;;
|
|
--bundle) shift; out="$1"; mode="file" ;;
|
|
--gh-issue) mode="gh-issue" ;;
|
|
*) die "unknown flag: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
local tmp; tmp=$(mktemp)
|
|
{
|
|
printf '# Larry-Anywhere lesson bundle\n\n'
|
|
printf 'Generated: %s\n' "$(date -Iseconds 2>/dev/null || date)"
|
|
printf 'Source: `%s` (host `%s`)\n\n' "$LESSONS_DIR" "$(hostname 2>/dev/null || echo unknown)"
|
|
printf 'Send this back to home-Larry (paste into chat) so it can be committed\n'
|
|
printf 'to the canonical agents/ persona files in cloverleaf-larry.\n\n'
|
|
printf '---\n\n'
|
|
|
|
# Walk files newest-first, optionally filtered by --since
|
|
find "$LESSONS_DIR" -maxdepth 1 -name '*.md' -type f 2>/dev/null \
|
|
| sort -r \
|
|
| while IFS= read -r f; do
|
|
local day; day=$(basename "$f" .md)
|
|
if [ -n "$since" ] && [ "$day" \< "$since" ]; then continue; fi
|
|
cat "$f"
|
|
printf '\n---\n\n'
|
|
done
|
|
} > "$tmp"
|
|
|
|
case "$mode" in
|
|
stdout) cat "$tmp"; rm -f "$tmp" ;;
|
|
file) mv "$tmp" "$out"; printf 'wrote bundle to %s\n' "$out" >&2 ;;
|
|
gh-issue)
|
|
if command -v gh >/dev/null 2>&1; then
|
|
gh issue create --repo bojj27/cloverleaf-larry \
|
|
--title "Lessons from $(hostname 2>/dev/null) — $(date +%Y-%m-%d)" \
|
|
--body-file "$tmp" \
|
|
--label "lessons" 2>&1
|
|
rm -f "$tmp"
|
|
else
|
|
printf 'gh not installed — falling back to stdout. Copy this body into a manual Issue:\n\n' >&2
|
|
cat "$tmp"
|
|
rm -f "$tmp"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_clear() {
|
|
local before=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--before) shift; before="$1" ;;
|
|
*) die "unknown flag: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
local files; files=$(find "$LESSONS_DIR" -maxdepth 1 -name '*.md' -type f 2>/dev/null | sort)
|
|
[ -n "$files" ] || { echo "no lesson files to clear"; return 0; }
|
|
|
|
if [ -z "$before" ]; then
|
|
printf 'clear ALL lesson files? '
|
|
printf '%s\n' "$files" | sed 's/^/ /'
|
|
printf 'proceed? [y/N]: '
|
|
read -r ans </dev/tty || ans=""
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
|
printf '%s\n' "$files" | xargs rm -f
|
|
# Reset index but keep header
|
|
head -1 "$LESSONS_INDEX" > "$LESSONS_INDEX.new"
|
|
mv "$LESSONS_INDEX.new" "$LESSONS_INDEX"
|
|
echo "cleared all lesson files"
|
|
else
|
|
printf 'clear lesson files BEFORE %s? files matched:\n' "$before"
|
|
local matched
|
|
matched=$(printf '%s\n' "$files" | while IFS= read -r f; do
|
|
local day; day=$(basename "$f" .md)
|
|
[ "$day" \< "$before" ] && echo "$f"
|
|
done)
|
|
[ -z "$matched" ] && { echo "(none)"; return 0; }
|
|
printf '%s\n' "$matched" | sed 's/^/ /'
|
|
printf 'proceed? [y/N]: '
|
|
read -r ans </dev/tty || ans=""
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
|
printf '%s\n' "$matched" | xargs rm -f
|
|
# Trim index entries
|
|
awk -F'\t' -v before="$before" 'NR==1 || $1 >= before' "$LESSONS_INDEX" > "$LESSONS_INDEX.new"
|
|
mv "$LESSONS_INDEX.new" "$LESSONS_INDEX"
|
|
echo "cleared lesson files before $before"
|
|
fi
|
|
}
|
|
|
|
cmd_count() {
|
|
local n; n=$(($(wc -l < "$LESSONS_INDEX") - 1))
|
|
echo "$n"
|
|
}
|
|
|
|
case "${1:-list}" in
|
|
add) shift; [ $# -ge 1 ] || die "usage: lessons.sh add \"text\" [--topic X --site Y --severity Z]"; text="$1"; shift; cmd_add "$text" "$@" ;;
|
|
list) cmd_list ;;
|
|
show) shift; cmd_show "${1:-}" ;;
|
|
export) shift; cmd_export "$@" ;;
|
|
clear) shift; cmd_clear "$@" ;;
|
|
count) cmd_count ;;
|
|
-h|--help|help) sed -n '2,30p' "$0" ;;
|
|
*) die "unknown subcommand: ${1:-}" ;;
|
|
esac
|