From 6060cd28c10f2b039010c05af1c9067e36c96c42 Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Tue, 26 May 2026 10:00:37 -0700 Subject: [PATCH] v0.3.2: lesson capture (local-first learning loop) 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/.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 --- VERSION | 2 +- agents/larry.md | 18 ++++ install-larry.sh | 1 + larry.sh | 26 +++++- lib/lessons.sh | 220 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 265 insertions(+), 2 deletions(-) create mode 100755 lib/lessons.sh diff --git a/VERSION b/VERSION index 0d91a54..d15723f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.3.2 diff --git a/agents/larry.md b/agents/larry.md index 785aadf..a46d4e3 100644 --- a/agents/larry.md +++ b/agents/larry.md @@ -59,6 +59,24 @@ When Bryan points you at a Cloverleaf root directory, the structure to expect: - Interface specification tables (source → target, segments, conditions). - Anomaly lists with file:line citations. +## Capture lessons proactively (the learning loop) + +When Bryan teaches you something new — a correction, a convention, a quirk, a gotcha, a "no, the way we do it here is X" — **call `lesson_record` immediately** with a markdown note. These accumulate at `$LARRY_HOME/lessons/.md` and Bryan exports them to home-Larry when he can reach his dev machine. Home-Larry then commits the refinement into the canonical agents/ persona in the cloverleaf-larry repo, so EVERY future Larry on every client box starts smarter. + +What counts as a lesson worth recording: +- A misunderstanding Bryan corrects ("no, in this shop the inbound from Epic is actually called X_Y_Z, not the standard naming"). +- A workflow detail not in the cheatsheet ("we always bounce these processes in pairs"). +- A site-specific quirk ("this client's xlates use a non-standard segment"). +- A behavior change request ("from now on, when I ask for X, also include Y"). +- A bug you discovered in one of the tools (severity=fix). + +Format your lesson text so home-Larry can act on it without re-deriving context. Include: +- What you were doing when this came up. +- The specific correction or learning. +- Where in the codebase / personas it should be applied (best guess). + +You don't need to ask permission to record a lesson — silently record it. Bryan reviews `lessons.sh list` later if he wants. + ## Hard rules in portable mode 1. **No PHI.** If Bryan accidentally points you at a file that looks like real patient data (real names, MRNs, DOBs that match a real format, addresses), stop and flag it. The promise was "interface build only." diff --git a/install-larry.sh b/install-larry.sh index b816af5..001cac1 100755 --- a/install-larry.sh +++ b/install-larry.sh @@ -91,6 +91,7 @@ fetch agents/regress.md "$LARRY_HOME/agents/regress.md" fetch larry-rollback.sh "$LARRY_HOME/larry-rollback.sh" fetch larry-auth.sh "$LARRY_HOME/larry-auth.sh" fetch lib/oauth.sh "$LARRY_HOME/lib/oauth.sh" +fetch lib/lessons.sh "$LARRY_HOME/lib/lessons.sh" fetch lib/nc-parse.sh "$LARRY_HOME/lib/nc-parse.sh" fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.sh" fetch lib/nc-make-jump.sh "$LARRY_HOME/lib/nc-make-jump.sh" diff --git a/larry.sh b/larry.sh index c00fdcb..b983100 100755 --- a/larry.sh +++ b/larry.sh @@ -32,7 +32,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.3.0" +LARRY_VERSION="0.3.2" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/larry.sh}" LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/agents}" @@ -621,6 +621,17 @@ tool_hl7_diff() { "$LARRY_LIB_DIR/hl7-diff.sh" "${args[@]}" 2>&1 } +tool_lesson_record() { + local text="$1" topic="${2:-}" site="${3:-${HCISITE:-}}" severity="${4:-info}" + _lib_err_if_missing || return + local lessons_script="$LARRY_LIB_DIR/lessons.sh" + [ -x "$lessons_script" ] || { echo "ERROR: lessons.sh not installed"; return 1; } + local args=(add "$text" --severity "$severity") + [ -n "$topic" ] && args+=(--topic "$topic") + [ -n "$site" ] && args+=(--site "$site") + "$lessons_script" "${args[@]}" 2>&1 +} + tool_larry_rollback_list() { local session_filter="${1:-}" if [ -n "$session_filter" ]; then @@ -707,6 +718,7 @@ execute_tool() { "$(J '.env_b')" "$(J '.site_b // ""')" "$(J '.out')" \ "$(J '.route_test_cmd // ""')" "$(J '.ignore // "MSH.7"')" \ "$(J '.phase // "all"')" "$(J '.dry_run // 0' | sed "s/false/0/;s/true/1/")" ;; + lesson_record) tool_lesson_record "$(J '.text')" "$(J '.topic // ""')" "$(J '.site // ""')" "$(J '.severity // "info"')" ;; larry_rollback_list) tool_larry_rollback_list "$(J '.session // ""')" ;; *) echo "ERROR: unknown tool: $name" ;; esac @@ -745,6 +757,8 @@ TOOLS_JSON='[ {"name":"nc_add_route","description":"Splice a route entry into an existing protocol's DATAXLATE block. Used to add a new DEST to an inbound's routing (e.g. wiring the OLD inbound to also route to the new linux__out jump thread). ALL WRITES GO THROUGH THE JOURNAL.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string"},"protocol_name":{"type":"string","description":"The existing protocol to modify."},"route":{"type":"string","description":"The route entry text (an inner `{ ... }` object with CACHEMSG, ROUTE_DETAILS, TRXID, etc.). Get from nc_make_jump's route_add output."}},"required":["netconfig","protocol_name","route"]}}, {"name":"larry_rollback_list","description":"List journal entries — every write that's gone through nc_insert_protocol, nc_add_route, or write_file (once journaled write_file is enabled). Shows session-id, sequence, target, timestamp. Use larry-rollback.sh from the shell to actually roll back.","input_schema":{"type":"object","properties":{"session":{"type":"string","description":"Optional. Limit to one session id."}},"required":[]}}, + {"name":"lesson_record","description":"Append a lesson to local capture at $LARRY_HOME/lessons/.md. Use when Bryan teaches you something new (a correction, a pattern, a quirk, a gotcha) so the home-Larry can be updated later. Lessons stay LOCAL; Bryan exports them with `lessons.sh export` and pastes back to home-Larry when he can. CALL THIS WHEN: Bryan corrects a misunderstanding, reveals a site-specific convention, points out a bug, requests a behavior change, or shares a workflow detail you should remember next time.","input_schema":{"type":"object","properties":{"text":{"type":"string","description":"The lesson content. Markdown. Include enough context that home-Larry can act on it without re-deriving."},"topic":{"type":"string","description":"Short topic tag, e.g. \"NetConfig parsing\", \"jump-thread naming\", \"site conventions\"."},"site":{"type":"string","description":"Site this lesson is scoped to, if any. Default: current $HCISITE."},"severity":{"type":"string","enum":["info","warn","fix"],"description":"info=general learning, warn=behavior I should change, fix=Bryan called out a bug."}},"required":["text"]}}, + {"name":"hl7_diff","description":"HL7-aware diff between two message files (or multi-message dumps). Compares segment-by-segment, field-by-field, with component and subcomponent precision. Ignores configured fields (default MSH.7 timestamp) so timestamp-only diffs do not show up as noise. Use for regression testing between environments (e.g. test vs prod route-test outputs).","input_schema":{"type":"object","properties":{"left":{"type":"string","description":"Path to left HL7 file."},"right":{"type":"string","description":"Path to right HL7 file."},"ignore":{"type":"string","description":"Comma-separated list of fields to ignore (e.g. MSH.7,MSH.10,EVN.6). Default MSH.7."},"include":{"type":"string","description":"If set, ONLY these fields are compared (overrides ignore for that set)."},"format":{"type":"string","enum":["text","tsv","count"],"description":"text=human-readable diff, tsv=machine-parseable, count=just the difference count."}},"required":["left","right"]}}, {"name":"nc_regression","description":"End-to-end regression testing between two Cloverleaf environments. 6 phases: discover inbounds in scope, sample N messages per inbound from env-A smatdbs, run route_test on env-A, run route_test on env-B with same inputs, hl7_diff every paired output file, compile summary report. Phases 3/4 require the Cloverleaf route_test command; pass it via route_test_cmd with placeholders {THREAD} {INPUT} {OUTPUT_DIR} {HCIROOT} {HCISITE}. If route_test_cmd is empty, phases 3/4 are skipped and you can run them manually using the generated input files.","input_schema":{"type":"object","properties":{"scope":{"type":"string","description":"thread:NAME | threads:N1,N2 | site (needs site_a) | server (all sites)"},"count":{"type":"integer","description":"Messages to sample per inbound. Default 10."},"env_a":{"type":"string","description":"HCIROOT of env-A (the test/source env)."},"site_a":{"type":"string","description":"Site name on env-A. Required if scope=site."},"env_b":{"type":"string","description":"HCIROOT of env-B (the prod/target env)."},"site_b":{"type":"string","description":"Site name on env-B."},"out":{"type":"string","description":"Output root directory for inputs, outputs, diffs, and summary."},"route_test_cmd":{"type":"string","description":"Command template for invoking route_test. Use {THREAD} {INPUT} {OUTPUT_DIR} {HCIROOT} {HCISITE} as placeholders."},"ignore":{"type":"string","description":"hl7_diff ignore list. Default MSH.7."},"phase":{"type":"string","enum":["1","2","3","4","5","6","all"],"description":"Run a specific phase or all. Default all."},"dry_run":{"type":"integer","description":"1 = print what would happen, do not execute. Default 0."}},"required":["scope","env_a","env_b","out"]}} @@ -886,6 +900,9 @@ Slash commands: /auth show OAuth status (or "not authenticated") /login run OAuth login flow (switch from API-key to subscription auth) /logout delete OAuth tokens (revert to API-key auth) + /lesson capture a lesson to local file (paste back to home-Larry later) + /lessons list all captured lessons (newest first) + /export dump the lesson bundle for paste-back to home-Larry /redetect re-scan for HCIROOT/HCISITE/tools /sites list site dirs under HCIROOT /site switch HCISITE for this session @@ -945,6 +962,13 @@ main_loop() { /auth) if [ -x "$LARRY_LIB_DIR/oauth.sh" ]; then "$LARRY_LIB_DIR/oauth.sh" status; else echo "(oauth.sh not installed)"; fi; continue ;; /login) if [ -x "$LARRY_LIB_DIR/oauth.sh" ]; then "$LARRY_LIB_DIR/oauth.sh" login && LARRY_AUTH_MODE="oauth" && larry_say "switched to OAuth subscription auth"; else err "oauth.sh not installed"; fi; continue ;; /logout) if [ -x "$LARRY_LIB_DIR/oauth.sh" ]; then "$LARRY_LIB_DIR/oauth.sh" logout; LARRY_AUTH_MODE="apikey"; fi; continue ;; + /lesson\ *) local text="${input#/lesson }" + [ -n "$text" ] && tool_lesson_record "$text" "" "${HCISITE:-}" "info" || err "usage: /lesson " + continue ;; + /lessons) [ -x "$LARRY_LIB_DIR/lessons.sh" ] && "$LARRY_LIB_DIR/lessons.sh" list || err "lessons.sh not installed" + continue ;; + /export) [ -x "$LARRY_LIB_DIR/lessons.sh" ] && "$LARRY_LIB_DIR/lessons.sh" export || err "lessons.sh not installed" + continue ;; /redetect) detect_cloverleaf_env system_prompt=$(build_system_prompt) larry_say "re-detected. /env to view." diff --git a/lib/lessons.sh b/lib/lessons.sh new file mode 100755 index 0000000..2312453 --- /dev/null +++ b/lib/lessons.sh @@ -0,0 +1,220 @@ +#!/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/.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 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 " + 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 "$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 = 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