Script for monitoring/summarizing uptsream updates with hermes
This commit is contained in:
Executable
+502
@@ -0,0 +1,502 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# hermes-upstream-watch.sh
|
||||
# Safe upstream watcher for hermes-agent.
|
||||
# Produces a markdown report for manual review before any pull.
|
||||
|
||||
set -u
|
||||
|
||||
# -----------------------------
|
||||
# Configuration (hardcoded)
|
||||
# -----------------------------
|
||||
REPO_DIR="/home/iadnah/.hermes/hermes-agent/"
|
||||
VAULT_DIR="/home/iadnah/lilaBuild/vaults/obsidian-private/hermes/"
|
||||
PAIN_FILE="${VAULT_DIR}/hermes-pain-points.md"
|
||||
REPORT_DIR="${VAULT_DIR}/reports/"
|
||||
|
||||
# -----------------------------
|
||||
# Argument parsing
|
||||
# -----------------------------
|
||||
QUIET=0
|
||||
if [ "${1:-}" = "--quiet" ]; then
|
||||
QUIET=1
|
||||
elif [ "${1:-}" != "" ]; then
|
||||
printf 'Usage: %s [--quiet]\n' "$0" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# -----------------------------
|
||||
# Utility helpers
|
||||
# -----------------------------
|
||||
trim() {
|
||||
local s="$1"
|
||||
s="${s#"${s%%[![:space:]]*}"}"
|
||||
s="${s%"${s##*[![:space:]]}"}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
add_unique_array() {
|
||||
local -n arr_ref="$1"
|
||||
local value="$2"
|
||||
local existing
|
||||
for existing in "${arr_ref[@]:-}"; do
|
||||
if [ "$existing" = "$value" ]; then
|
||||
return
|
||||
fi
|
||||
done
|
||||
arr_ref+=("$value")
|
||||
}
|
||||
|
||||
join_by() {
|
||||
local delimiter="$1"
|
||||
shift || true
|
||||
local out=""
|
||||
local item
|
||||
for item in "$@"; do
|
||||
if [ -z "$out" ]; then
|
||||
out="$item"
|
||||
else
|
||||
out="${out}${delimiter}${item}"
|
||||
fi
|
||||
done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
is_number() {
|
||||
case "$1" in
|
||||
''|*[!0-9]*) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# -----------------------------
|
||||
# Validate core commands
|
||||
# -----------------------------
|
||||
for required_cmd in curl jq git date grep cat mkdir; do
|
||||
if ! command -v "$required_cmd" >/dev/null 2>&1; then
|
||||
printf 'Hard failure: required command not found: %s\n' "$required_cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# -----------------------------
|
||||
# Ensure directories/files exist
|
||||
# -----------------------------
|
||||
if ! mkdir -p "$REPORT_DIR"; then
|
||||
printf 'Hard failure: could not create reports directory: %s\n' "$REPORT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PAIN_FILE" ]; then
|
||||
if ! cat >"$PAIN_FILE" <<'EOF'
|
||||
# Hermes Pain Points & Interests
|
||||
|
||||
## Areas of Interest (keywords/phrases that should always be flagged)
|
||||
- gateway
|
||||
- skills config
|
||||
- skill config
|
||||
- telegram
|
||||
- provider error
|
||||
- provider errors
|
||||
- memory
|
||||
- honcho
|
||||
- mcp
|
||||
- profile
|
||||
- cron
|
||||
- scheduler
|
||||
- auth
|
||||
- authentication
|
||||
|
||||
## Known Pain Points & Workarounds (format: date | topic | description)
|
||||
- 2026-05-10 | Telegram gateway | Local patch for message ordering race condition. Upstream commit touching gateway/ should be reviewed carefully.
|
||||
- 2026-04-29 | MCP connection | Timeout on thor.uplinklounge.com for emailInbox-iadnah. Check any networking or MCP changes.
|
||||
EOF
|
||||
then
|
||||
printf 'Hard failure: could not write pain-points file: %s\n' "$PAIN_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------
|
||||
# Parse pain-points file
|
||||
# -----------------------------
|
||||
interest_keywords=()
|
||||
pain_dates=()
|
||||
pain_topics=()
|
||||
pain_descriptions=()
|
||||
|
||||
section=""
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
"## Areas of Interest"*)
|
||||
section="interest"
|
||||
continue
|
||||
;;
|
||||
"## Known Pain Points"*)
|
||||
section="pain"
|
||||
continue
|
||||
;;
|
||||
"## "*)
|
||||
section=""
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$section" = "interest" ]; then
|
||||
case "$line" in
|
||||
"- "*)
|
||||
kw="$(trim "${line#- }")"
|
||||
if [ -n "$kw" ]; then
|
||||
add_unique_array interest_keywords "$kw"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
elif [ "$section" = "pain" ]; then
|
||||
case "$line" in
|
||||
"- "*)
|
||||
entry="${line#- }"
|
||||
IFS='|' read -r p_date p_topic p_desc <<<"$entry"
|
||||
p_date="$(trim "$p_date")"
|
||||
p_topic="$(trim "$p_topic")"
|
||||
p_desc="$(trim "$p_desc")"
|
||||
if [ -n "$p_date" ] && [ -n "$p_topic" ]; then
|
||||
pain_dates+=("$p_date")
|
||||
pain_topics+=("$p_topic")
|
||||
pain_descriptions+=("$p_desc")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done <"$PAIN_FILE"
|
||||
|
||||
# -----------------------------
|
||||
# Gather local git state
|
||||
# -----------------------------
|
||||
git_available=1
|
||||
git_warnings=()
|
||||
branch_name="unknown"
|
||||
local_dirty="unknown"
|
||||
behind_count="unknown"
|
||||
ahead_count="unknown"
|
||||
local_head_sha="unknown"
|
||||
local_head_date="unknown"
|
||||
upstream_latest_sha="unknown"
|
||||
upstream_latest_date="unknown"
|
||||
git_status_full="git status unavailable"
|
||||
git_log_last20="git log unavailable"
|
||||
|
||||
if [ ! -d "${REPO_DIR}/.git" ]; then
|
||||
git_available=0
|
||||
add_unique_array git_warnings "Local repository missing or invalid at ${REPO_DIR}"
|
||||
else
|
||||
if ! git -C "$REPO_DIR" fetch origin --quiet >/dev/null 2>&1; then
|
||||
add_unique_array git_warnings "git fetch origin failed (network/auth issue possible)."
|
||||
fi
|
||||
|
||||
branch_name="$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || printf 'unknown')"
|
||||
status_porcelain="$(git -C "$REPO_DIR" status --porcelain 2>/dev/null || printf '')"
|
||||
if [ -z "$status_porcelain" ]; then
|
||||
local_dirty="clean"
|
||||
else
|
||||
local_dirty="dirty"
|
||||
fi
|
||||
|
||||
behind_count="$(git -C "$REPO_DIR" rev-list --count HEAD..origin/main 2>/dev/null || printf 'unknown')"
|
||||
ahead_count="$(git -C "$REPO_DIR" rev-list --count origin/main..HEAD 2>/dev/null || printf 'unknown')"
|
||||
local_head_sha="$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || printf 'unknown')"
|
||||
local_head_date="$(git -C "$REPO_DIR" show -s --format=%cs HEAD 2>/dev/null || printf 'unknown')"
|
||||
upstream_latest_sha="$(git -C "$REPO_DIR" rev-parse --short origin/main 2>/dev/null || printf 'unknown')"
|
||||
upstream_latest_date="$(git -C "$REPO_DIR" show -s --format=%cs origin/main 2>/dev/null || printf 'unknown')"
|
||||
git_status_full="$(git -C "$REPO_DIR" status 2>&1 || printf 'git status command failed')"
|
||||
git_log_last20="$(git -C "$REPO_DIR" log --oneline --decorate -20 2>&1 || printf 'git log command failed')"
|
||||
|
||||
if [ "$branch_name" != "main" ]; then
|
||||
add_unique_array git_warnings "Local repository is on branch '${branch_name}' (expected main)."
|
||||
fi
|
||||
if [ "$local_dirty" = "dirty" ]; then
|
||||
add_unique_array git_warnings "Local repository has uncommitted changes."
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------
|
||||
# Fetch upstream commits (last 24h)
|
||||
# -----------------------------
|
||||
since_utc="$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)"
|
||||
api_url="https://api.github.com/repos/NousResearch/hermes-agent/commits?since=${since_utc}&per_page=100"
|
||||
api_json=""
|
||||
api_available=1
|
||||
api_note=""
|
||||
total_commits=0
|
||||
|
||||
if ! api_json="$(curl --max-time 15 -s "$api_url" 2>/dev/null)"; then
|
||||
api_available=0
|
||||
api_note="API unavailable"
|
||||
api_json='{"error":"API unavailable"}'
|
||||
fi
|
||||
|
||||
if [ "$api_available" -eq 1 ]; then
|
||||
if ! printf '%s' "$api_json" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
||||
api_available=0
|
||||
api_note="API unavailable"
|
||||
else
|
||||
total_commits="$(printf '%s' "$api_json" | jq -r 'length' 2>/dev/null || printf '0')"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$api_available" -eq 0 ]; then
|
||||
total_commits=0
|
||||
fi
|
||||
|
||||
# -----------------------------
|
||||
# Relevance and pain-point matching
|
||||
# -----------------------------
|
||||
relevant_entries=()
|
||||
other_entries=()
|
||||
crosscheck_entries=()
|
||||
relevant_count=0
|
||||
|
||||
if [ "$api_available" -eq 1 ] && [ "$total_commits" -gt 0 ]; then
|
||||
commit_json_lines="$(printf '%s' "$api_json" | jq -c '.[]')"
|
||||
|
||||
while IFS= read -r commit_json; do
|
||||
[ -z "$commit_json" ] && continue
|
||||
|
||||
sha_full="$(printf '%s' "$commit_json" | jq -r '.sha // ""')"
|
||||
author_name="$(printf '%s' "$commit_json" | jq -r '.commit.author.name // "unknown"')"
|
||||
author_date="$(printf '%s' "$commit_json" | jq -r '.commit.author.date // "unknown"')"
|
||||
commit_message="$(printf '%s' "$commit_json" | jq -r '.commit.message // ""')"
|
||||
[ -z "$sha_full" ] && continue
|
||||
|
||||
sha_short="${sha_full:0:7}"
|
||||
commit_title="${commit_message%%$'\n'*}"
|
||||
if [ "$commit_message" = "$commit_title" ]; then
|
||||
commit_body=""
|
||||
else
|
||||
commit_body="${commit_message#*$'\n'}"
|
||||
fi
|
||||
commit_body_one_line="${commit_body//$'\n'/ }"
|
||||
commit_body_preview="${commit_body_one_line:0:80}"
|
||||
|
||||
match_tags=()
|
||||
path_tags=()
|
||||
overlap_notes=()
|
||||
|
||||
combined_text="${commit_title} ${commit_body_one_line}"
|
||||
combined_text_lower="${combined_text,,}"
|
||||
|
||||
for keyword in "${interest_keywords[@]:-}"; do
|
||||
keyword_lower="${keyword,,}"
|
||||
if [ -n "$keyword_lower" ] && [[ "$combined_text_lower" == *"$keyword_lower"* ]]; then
|
||||
add_unique_array match_tags "$keyword"
|
||||
fi
|
||||
done
|
||||
|
||||
changed_files=""
|
||||
if [ "$git_available" -eq 1 ] && git -C "$REPO_DIR" cat-file -e "${sha_full}^{commit}" >/dev/null 2>&1; then
|
||||
changed_files="$(git -C "$REPO_DIR" show --name-only --pretty="" "$sha_full" 2>/dev/null || printf '')"
|
||||
fi
|
||||
|
||||
if [ -n "$changed_files" ]; then
|
||||
while IFS= read -r changed_path || [ -n "$changed_path" ]; do
|
||||
[ -z "$changed_path" ] && continue
|
||||
changed_path_lower="${changed_path,,}"
|
||||
case "$changed_path_lower" in
|
||||
*"gateway/"*) add_unique_array path_tags "gateway" ;;
|
||||
esac
|
||||
case "$changed_path_lower" in
|
||||
*"skills/"*) add_unique_array path_tags "skills" ;;
|
||||
esac
|
||||
case "$changed_path_lower" in
|
||||
*"cron"*) add_unique_array path_tags "cron" ;;
|
||||
esac
|
||||
case "$changed_path_lower" in
|
||||
*"memory"*) add_unique_array path_tags "memory" ;;
|
||||
esac
|
||||
case "$changed_path_lower" in
|
||||
*"honcho"*) add_unique_array path_tags "honcho" ;;
|
||||
esac
|
||||
case "$changed_path_lower" in
|
||||
*"mcp"*) add_unique_array path_tags "mcp" ;;
|
||||
esac
|
||||
case "$changed_path_lower" in
|
||||
*"telegram"*) add_unique_array path_tags "telegram" ;;
|
||||
esac
|
||||
done <<<"$changed_files"
|
||||
fi
|
||||
|
||||
context_text_lower="${combined_text_lower} ${changed_files,,}"
|
||||
idx=0
|
||||
while [ "$idx" -lt "${#pain_topics[@]}" ]; do
|
||||
topic="${pain_topics[$idx]}"
|
||||
topic_lower="${topic,,}"
|
||||
if [ -n "$topic_lower" ] && [[ "$context_text_lower" == *"$topic_lower"* ]]; then
|
||||
note="${pain_dates[$idx]} | ${pain_topics[$idx]}"
|
||||
add_unique_array overlap_notes "$note"
|
||||
fi
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
is_relevant=0
|
||||
if [ "${#match_tags[@]}" -gt 0 ] || [ "${#path_tags[@]}" -gt 0 ] || [ "${#overlap_notes[@]}" -gt 0 ]; then
|
||||
is_relevant=1
|
||||
fi
|
||||
|
||||
if [ "$is_relevant" -eq 1 ]; then
|
||||
relevant_count=$((relevant_count + 1))
|
||||
|
||||
combined_matches=("${match_tags[@]:-}" "${path_tags[@]:-}")
|
||||
unique_matches=()
|
||||
for m in "${combined_matches[@]:-}"; do
|
||||
[ -z "$m" ] && continue
|
||||
add_unique_array unique_matches "$m"
|
||||
done
|
||||
matches_text="$(join_by ', ' "${unique_matches[@]:-}")"
|
||||
if [ -z "$matches_text" ]; then
|
||||
matches_text="pain-point overlap"
|
||||
fi
|
||||
|
||||
entry="- **[${matches_text}]** \`${sha_short}\` - ${commit_title}"
|
||||
if [ -n "$commit_body_preview" ]; then
|
||||
entry="${entry}\n Body: ${commit_body_preview}"
|
||||
fi
|
||||
if [ "${#overlap_notes[@]}" -gt 0 ]; then
|
||||
for ov in "${overlap_notes[@]}"; do
|
||||
ov_date="$(trim "${ov%%|*}")"
|
||||
ov_topic="$(trim "${ov#*|}")"
|
||||
entry="${entry}\n → ⚠️ Overlaps with known pain point from ${ov_date} (${ov_topic})."
|
||||
add_unique_array crosscheck_entries "- ⚠️ ${ov_date} | ${ov_topic} | Commit \`${sha_short}\` (${commit_title})"
|
||||
done
|
||||
fi
|
||||
add_unique_array relevant_entries "$entry"
|
||||
else
|
||||
add_unique_array other_entries "- \`${sha_short}\` - ${commit_title} (${author_name}, ${author_date})"
|
||||
fi
|
||||
done <<<"$commit_json_lines"
|
||||
fi
|
||||
|
||||
# -----------------------------
|
||||
# Quiet mode gate
|
||||
# -----------------------------
|
||||
create_report=1
|
||||
behind_for_gate=0
|
||||
if is_number "$behind_count"; then
|
||||
behind_for_gate="$behind_count"
|
||||
fi
|
||||
|
||||
if [ "$QUIET" -eq 1 ]; then
|
||||
create_report=0
|
||||
if [ "$relevant_count" -gt 0 ] || [ "$behind_for_gate" -ge 3 ]; then
|
||||
create_report=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$create_report" -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# -----------------------------
|
||||
# Build recommendation text
|
||||
# -----------------------------
|
||||
recommendation="Review upstream commits before pulling."
|
||||
if [ "$api_available" -eq 0 ]; then
|
||||
recommendation="API unavailable; defer pull decisions until connectivity returns and rerun this watcher."
|
||||
elif is_number "$behind_count" && [ "$behind_count" -gt 0 ] && [ "$relevant_count" -gt 0 ]; then
|
||||
recommendation="Review the ${behind_count} commits before pulling. Relevant changes were detected in sensitive areas and should be tested first."
|
||||
elif is_number "$behind_count" && [ "$behind_count" -gt 0 ]; then
|
||||
recommendation="Review the ${behind_count} commits before pulling. No high-signal pain-point overlaps were found, but a normal verification pass is advised."
|
||||
elif [ "$relevant_count" -gt 0 ]; then
|
||||
recommendation="Recent commits match tracked interests/pain points. Review these changes before pulling even if branch divergence is low."
|
||||
fi
|
||||
|
||||
# -----------------------------
|
||||
# Generate markdown report
|
||||
# -----------------------------
|
||||
run_date="$(date -u +%Y-%m-%d)"
|
||||
generated_at="$(date -u '+%Y-%m-%d %H:%M UTC')"
|
||||
report_path="${REPORT_DIR}/hermes-upstream-${run_date}.md"
|
||||
|
||||
local_changes_line="$local_dirty"
|
||||
if [ "$local_dirty" = "dirty" ]; then
|
||||
local_changes_line="dirty (warn: uncommitted changes present)"
|
||||
elif [ "$local_dirty" = "clean" ]; then
|
||||
local_changes_line="clean"
|
||||
fi
|
||||
|
||||
{
|
||||
printf '# Hermes-Agent Upstream Watch - %s\n\n' "$run_date"
|
||||
printf '**Generated**: %s\n' "$generated_at"
|
||||
printf '**Local HEAD**: %s (%s)\n' "$local_head_sha" "$local_head_date"
|
||||
printf '**Upstream latest**: %s (%s)\n' "$upstream_latest_sha" "$upstream_latest_date"
|
||||
printf '**Commits behind**: %s\n' "$behind_count"
|
||||
printf '**Commits ahead**: %s\n' "$ahead_count"
|
||||
printf '**Local changes**: %s\n\n' "$local_changes_line"
|
||||
|
||||
if [ "${#git_warnings[@]}" -gt 0 ]; then
|
||||
for warn in "${git_warnings[@]}"; do
|
||||
printf '> ⚠️ %s\n' "$warn"
|
||||
done
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
printf '## Recent Upstream Activity (last 24h)\n'
|
||||
if [ "$api_available" -eq 1 ]; then
|
||||
printf 'Total commits: %s\n\n' "$total_commits"
|
||||
else
|
||||
printf 'Total commits: 0\n\n'
|
||||
printf '> API unavailable\n\n'
|
||||
fi
|
||||
|
||||
printf '## Relevant / Interesting Changes\n'
|
||||
if [ "${#relevant_entries[@]}" -eq 0 ]; then
|
||||
printf -- '- No relevant commits matched current interests/pain points.\n\n'
|
||||
else
|
||||
for entry in "${relevant_entries[@]}"; do
|
||||
printf '%b\n\n' "$entry"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "${#other_entries[@]}" -gt 0 ]; then
|
||||
printf '### Other commits\n'
|
||||
for other in "${other_entries[@]}"; do
|
||||
printf '%s\n' "$other"
|
||||
done
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
printf '## Pain Point Cross-Check\n'
|
||||
if [ "${#crosscheck_entries[@]}" -eq 0 ]; then
|
||||
printf -- '- No direct overlaps detected with known pain points.\n\n'
|
||||
else
|
||||
for cross in "${crosscheck_entries[@]}"; do
|
||||
printf '%s\n' "$cross"
|
||||
done
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
printf '## Recommendation\n'
|
||||
printf '%s\n\n' "$recommendation"
|
||||
printf '**Full raw commit data** and git status saved below for reference.\n\n'
|
||||
|
||||
printf '<details>\n'
|
||||
printf '<summary>Raw Data</summary>\n\n'
|
||||
printf '### GitHub API JSON\n'
|
||||
printf '```json\n'
|
||||
printf '%s\n' "$api_json"
|
||||
printf '```\n\n'
|
||||
|
||||
printf '### Git Log (last 20 commits)\n'
|
||||
printf '```text\n'
|
||||
printf '%s\n' "$git_log_last20"
|
||||
printf '```\n\n'
|
||||
|
||||
printf '### Git Status\n'
|
||||
printf '```text\n'
|
||||
printf '%s\n' "$git_status_full"
|
||||
printf '```\n\n'
|
||||
printf '</details>\n'
|
||||
} >"$report_path"
|
||||
|
||||
if [ "$QUIET" -eq 0 ] || [ "$relevant_count" -gt 0 ] || [ "$behind_for_gate" -ge 3 ]; then
|
||||
printf 'Report written to %s\n' "$report_path"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user