Script for monitoring/summarizing uptsream updates with hermes

This commit is contained in:
Jason Thistlethwaite
2026-05-15 21:03:58 -04:00
parent e4382b98e8
commit 1f235064e2
6 changed files with 775 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# cronScripts Repository Conventions
This repository is the source-of-truth for script jobs and automation tasks.
## Required structure
- `~/projects/cronScripts/` contains one folder per task/script/job.
- Each task folder contains the shippable, finished copy of the work product(s).
- Runtime/deployed copies can exist elsewhere (for example `~/.hermes/scripts/`), but this repo must keep the canonical version under the task folder.
## Required files per task folder
Every task folder must include:
- `README.md` - what the script/job is and how it works.
- `INSTALL.md` - exact setup/install steps to begin using it.
- `HANDOFF.md` - brief status and checklist of completed + pending work.
## HANDOFF policy (mandatory)
- `HANDOFF.md` must exist for every script/task folder.
- `HANDOFF.md` must be updated whenever any agent modifies anything in this git repo.
- Updates must include:
- what was changed (checked items), and
- any remaining next steps (unchecked items).
## Practical rule
Regardless of external install/use instructions, the finished, shippable copy of each script/job must be committed in this repo's task-folder structure.
+18
View File
@@ -0,0 +1,18 @@
# Handoff
## Last work completed
- [x] Built `hermes-upstream-watch.sh` to match the watcher specification.
- [x] Deployed runnable copy at `/home/iadnah/.hermes/scripts/hermes-upstream-watch.sh`.
- [x] Added shippable repo copy at `hermes-repo-watcher/hermes-upstream-watch.sh`.
- [x] Verified Bash syntax with `bash -n`.
- [x] Executed normal mode once and confirmed report generation.
- [x] Executed quiet mode once and confirmed conditional output behavior.
- [x] Added project docs: `README.md` and `INSTALL.md`.
## Next steps (not yet completed)
- [ ] Add/refresh pain-point keywords in `hermes-pain-points.md` to reflect latest local issues.
- [ ] Create or verify the daily 8:00 cron job in Hermes.
- [ ] Confirm destination delivery for cron output (`deliver=origin`) reaches the intended chat.
- [ ] Add this folder to CI checks (optional) to run shell lint/syntax validation on future edits.
+52
View File
@@ -0,0 +1,52 @@
# Install and Setup
This folder stores the shippable copy for the Hermes upstream watcher job.
## 1) Copy script into Hermes scripts directory
```bash
mkdir -p /home/iadnah/.hermes/scripts
cp /home/iadnah/projects/cronScripts/hermes-repo-watcher/hermes-upstream-watch.sh /home/iadnah/.hermes/scripts/hermes-upstream-watch.sh
chmod +x /home/iadnah/.hermes/scripts/hermes-upstream-watch.sh
```
## 2) Run once to bootstrap and verify
```bash
/home/iadnah/.hermes/scripts/hermes-upstream-watch.sh
```
Expected behavior:
- Creates `/home/iadnah/lilaBuild/vaults/obsidian-private/hermes/reports/` if missing.
- Creates `/home/iadnah/lilaBuild/vaults/obsidian-private/hermes/hermes-pain-points.md` if missing.
- Writes a report file named `hermes-upstream-YYYY-MM-DD.md`.
## 3) Optional quiet mode test
```bash
/home/iadnah/.hermes/scripts/hermes-upstream-watch.sh --quiet
```
In quiet mode, a report is only produced when either:
- relevant commits are detected, or
- local branch is at least 3 commits behind `origin/main`.
## 4) Schedule daily at 8:00 with Hermes cron
CLI:
```bash
hermes cron create "0 8 * * *" \
--no-agent \
--script hermes-upstream-watch.sh \
--deliver origin \
--name "hermes-upstream-watch"
```
Tool form:
```text
cronjob(action="create", schedule="0 8 * * *", script="hermes-upstream-watch.sh", no_agent=true, deliver="origin", name="hermes-upstream-watch")
```
+36
View File
@@ -0,0 +1,36 @@
# Hermes Repo Watcher
`hermes-upstream-watch.sh` is a standalone Bash watchdog that summarizes upstream `NousResearch/hermes-agent` activity and writes a daily Markdown report for safe human review.
It is designed to answer one question before any update: "Is pulling upstream worth the risk today?"
## What it does
- Queries GitHub commits from the last 24 hours (max 100) with a 15s network timeout.
- Reads local repo state from `/home/iadnah/.hermes/hermes-agent/` without changing history.
- Computes branch divergence (`behind` / `ahead`), local branch name, and dirty/clean status.
- Cross-checks commit titles/bodies and changed paths against a pain-points file.
- Produces a report at `/home/iadnah/lilaBuild/vaults/obsidian-private/hermes/reports/hermes-upstream-YYYY-MM-DD.md`.
- In `--quiet` mode, stays silent unless relevant changes were found or local is 3+ commits behind.
## Safety guarantees
- Never runs `git pull`, `git merge`, or `git reset`.
- Uses `git fetch origin --quiet` only to refresh remote refs for comparison.
- Handles API and git failures by writing clear warnings into the report (including `API unavailable`).
## Inputs and outputs
- Script path (deployed copy): `/home/iadnah/.hermes/scripts/hermes-upstream-watch.sh`
- Repo copy in this git folder: `hermes-repo-watcher/hermes-upstream-watch.sh`
- Pain-point file: `/home/iadnah/lilaBuild/vaults/obsidian-private/hermes/hermes-pain-points.md`
- Report directory: `/home/iadnah/lilaBuild/vaults/obsidian-private/hermes/reports/`
If `hermes-pain-points.md` is missing, the script auto-creates a starter template.
## Usage
```bash
/home/iadnah/.hermes/scripts/hermes-upstream-watch.sh
/home/iadnah/.hermes/scripts/hermes-upstream-watch.sh --quiet
```
+138
View File
@@ -0,0 +1,138 @@
**INSTRUCTIONS FOR CODEX: Build `hermes-upstream-watch` Script**
You are Codex (OpenAI Codex CLI). Your task is to write a **self-contained, production-ready script** exactly according to this specification. Do not add extra LLM calls, complex pipelines, or external dependencies beyond what is listed. The script must be simple, reliable, and suitable for cron or the `cronjob` tool.
### Overall Goal (Context You Do Not Have)
We maintain a local git clone of the hermes-agent repo at `/home/iadnah/.hermes/hermes-agent/`. We **never** want to `git pull` blindly because upstream changes (especially to gateway, skills, Telegram integration, providers, memory/Honcho, or MCP) can introduce breakage that costs us hours of debugging.
The purpose of this script is to **safely summarize upstream changes** so that Lila (the dominant AI collaborator) and the user can review them and decide together whether a pull is worth the risk. The script produces a human-readable Markdown report. It cross-references changes against a private "pain points" vault file that tracks bugs, workarounds, and areas we care about. It must **never** perform a pull, merge, or any destructive git operation.
This script will be run via the Hermes `cronjob` tool or directly. It should be lightweight (bash + standard tools like `curl`, `jq`, `git`). It should leverage existing Hermes patterns (output Markdown to the private vault, use absolute paths, be silent unless there is something interesting).
### Core Requirements
- **Location**: Write the script to `/home/iadnah/.hermes/scripts/hermes-upstream-watch.sh`
- **Shebang**: `#!/usr/bin/env bash`
- **Make executable** (include `chmod +x` instruction at the end of your response).
- **Configuration**: Use these hardcoded absolute paths (do not make them configurable via args for the first version):
- Local hermes-agent clone: `/home/iadnah/.hermes/hermes-agent/`
- Private vault directory: `/home/iadnah/lilaBuild/vaults/obsidian-private/hermes/`
- Pain points / interests file: `${VAULT_DIR}/hermes-pain-points.md` (create this file with example content if it does not exist — see format below).
- Reports directory: `${VAULT_DIR}/reports/` (create if missing). Each run saves a timestamped report like `hermes-upstream-2026-05-16.md`.
- **Runtime**: Must work in a clean environment with only `curl`, `jq`, `git`, `date`, `grep`, `cat`, `mkdir`. No Python, no extra pip packages.
- **Safety**:
- Never run `git pull`, `git merge`, `git reset`, or anything that changes the repo.
- If the local repo has uncommitted changes or is not on `main`, print a clear warning in the report.
- Timeout all network calls (`curl --max-time 15`).
- Handle missing files or git failures gracefully with clear messages.
- **Cron-friendly**:
- Accept an optional `--quiet` flag. In quiet mode, only produce a report if there are relevant commits or we are >3 commits behind.
- Exit code 0 on success, non-zero on hard failure.
- All output goes to the Markdown report + optional stdout summary.
### Detailed Behavior
1. **Create directories and pain-points file if missing**
- Ensure `${VAULT_DIR}/reports/` exists.
- If `hermes-pain-points.md` does not exist, create it with this skeleton (you can expand the example content):
```markdown
# 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.
```
2. **Fetch upstream commits (last 24 hours)**
- Run the exact curl from the request:
```bash
curl --max-time 15 -s "https://api.github.com/repos/NousResearch/hermes-agent/commits?since=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)&per_page=100"
```
- Parse with `jq`. For each commit, capture: sha (short), author, date, title, body (first 80 chars).
- Also fetch the latest upstream SHA via git.
3. **Local git status**
- `cd /home/iadnah/.hermes/hermes-agent && git fetch origin --quiet`
- Compute:
- Commits behind: `git rev-list --count HEAD..origin/main`
- Commits ahead (if any).
- Current branch and whether there are uncommitted changes (`git status --porcelain`).
- Current local HEAD short sha and date.
4. **Relevance & Pain-Point Matching**
- Read the "Areas of Interest" section from the pain-points file.
- For every upstream commit, check (case-insensitive) if title or body contains any keyword.
- Also scan changed files if possible (`git log --name-only` for that commit) and flag if they touch `gateway/`, `skills/`, `cron/`, `memory/`, `honcho`, `mcp`, etc.
- Cross-reference against the "Known Pain Points" section: if a commit touches a listed topic, add a warning note with the date of the pain point.
5. **Generate Report**
- Output a clean Markdown file with this exact structure (use the date of the run):
```markdown
# Hermes-Agent Upstream Watch - 2026-05-16
**Generated**: 2026-05-16 09:15 UTC
**Local HEAD**: abc1234 (2026-05-15)
**Upstream latest**: def5678 (2026-05-16)
**Commits behind**: 7
**Commits ahead**: 0
**Local changes**: clean / dirty (warn if dirty)
## Recent Upstream Activity (last 24h)
Total commits: 4
## Relevant / Interesting Changes
- **[gateway]** `8f3c2d1` - Fix race condition in Telegram message queue (matches: gateway, telegram)
→ ⚠️ Overlaps with known pain point from 2026-05-10 (message ordering).
- (list only relevant ones first, then optionally a short "Other commits" section)
## Pain Point Cross-Check
(list any overlaps with clear warnings)
## Recommendation
Review the 7 commits before pulling. The gateway change looks useful but requires testing our local Telegram workaround.
**Full raw commit data** and git status saved below for reference.
```
- At the bottom, include a collapsible "Raw Data" section with the full JSON from the API and full `git log` output (last 20 commits).
6. **Final Steps**
- Print a short stdout message: "Report written to /path/to/report.md"
- In quiet mode, only print if relevant changes exist or behind >= 3.
### What NOT to Do
- Do not build any LLM summarization, API calls to models, or complex Python pipelines inside the script.
- Do not add interactive prompts.
- Do not assume the script runs inside Hermes — it must be a standalone shell script.
- Do not hardcode any Lila-specific persona text.
### Acceptance Criteria
- Script runs cleanly with `./hermes-upstream-watch.sh` and `./hermes-upstream-watch.sh --quiet`.
- Produces exactly one timestamped Markdown report per run in the vault.
- Correctly identifies commits matching the interest keywords and pain points.
- Handles network failure (shows "API unavailable" in report).
- Follows the exact report structure above.
- Includes comments in the script explaining each major section.
After you write the script, also output:
1. The exact command to make it executable and test it.
2. A suggested cron entry (for the Hermes `cronjob` tool) that runs it daily at 8am and delivers the report back to this chat.
This specification is complete and self-contained. Build exactly this — no more, no less.
+502
View File
@@ -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