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/<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>
This commit is contained in:
parent
61f1500492
commit
6060cd28c1
@ -59,6 +59,24 @@ When Bryan points you at a Cloverleaf root directory, the structure to expect:
|
|||||||
- Interface specification tables (source → target, segments, conditions).
|
- Interface specification tables (source → target, segments, conditions).
|
||||||
- Anomaly lists with file:line citations.
|
- 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/<date>.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
|
## 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."
|
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."
|
||||||
|
|||||||
@ -91,6 +91,7 @@ fetch agents/regress.md "$LARRY_HOME/agents/regress.md"
|
|||||||
fetch larry-rollback.sh "$LARRY_HOME/larry-rollback.sh"
|
fetch larry-rollback.sh "$LARRY_HOME/larry-rollback.sh"
|
||||||
fetch larry-auth.sh "$LARRY_HOME/larry-auth.sh"
|
fetch larry-auth.sh "$LARRY_HOME/larry-auth.sh"
|
||||||
fetch lib/oauth.sh "$LARRY_HOME/lib/oauth.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-parse.sh "$LARRY_HOME/lib/nc-parse.sh"
|
||||||
fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.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"
|
fetch lib/nc-make-jump.sh "$LARRY_HOME/lib/nc-make-jump.sh"
|
||||||
|
|||||||
26
larry.sh
26
larry.sh
@ -32,7 +32,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.3.0"
|
LARRY_VERSION="0.3.2"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/larry.sh}"
|
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}"
|
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
|
"$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() {
|
tool_larry_rollback_list() {
|
||||||
local session_filter="${1:-}"
|
local session_filter="${1:-}"
|
||||||
if [ -n "$session_filter" ]; then
|
if [ -n "$session_filter" ]; then
|
||||||
@ -707,6 +718,7 @@ execute_tool() {
|
|||||||
"$(J '.env_b')" "$(J '.site_b // ""')" "$(J '.out')" \
|
"$(J '.env_b')" "$(J '.site_b // ""')" "$(J '.out')" \
|
||||||
"$(J '.route_test_cmd // ""')" "$(J '.ignore // "MSH.7"')" \
|
"$(J '.route_test_cmd // ""')" "$(J '.ignore // "MSH.7"')" \
|
||||||
"$(J '.phase // "all"')" "$(J '.dry_run // 0' | sed "s/false/0/;s/true/1/")" ;;
|
"$(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 // ""')" ;;
|
larry_rollback_list) tool_larry_rollback_list "$(J '.session // ""')" ;;
|
||||||
*) echo "ERROR: unknown tool: $name" ;;
|
*) echo "ERROR: unknown tool: $name" ;;
|
||||||
esac
|
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_<tag>_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":"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_<tag>_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":"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/<date>.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":"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"]}}
|
{"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")
|
/auth show OAuth status (or "not authenticated")
|
||||||
/login run OAuth login flow (switch from API-key to subscription auth)
|
/login run OAuth login flow (switch from API-key to subscription auth)
|
||||||
/logout delete OAuth tokens (revert to API-key auth)
|
/logout delete OAuth tokens (revert to API-key auth)
|
||||||
|
/lesson <text> 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
|
/redetect re-scan for HCIROOT/HCISITE/tools
|
||||||
/sites list site dirs under HCIROOT
|
/sites list site dirs under HCIROOT
|
||||||
/site <name> switch HCISITE for this session
|
/site <name> 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 ;;
|
/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 ;;
|
/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 ;;
|
/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 <text>"
|
||||||
|
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
|
/redetect) detect_cloverleaf_env
|
||||||
system_prompt=$(build_system_prompt)
|
system_prompt=$(build_system_prompt)
|
||||||
larry_say "re-detected. /env to view."
|
larry_say "re-detected. /env to view."
|
||||||
|
|||||||
220
lib/lessons.sh
Executable file
220
lib/lessons.sh
Executable file
@ -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/<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
|
||||||
Loading…
Reference in New Issue
Block a user