Script for monitoring/summarizing uptsream updates with hermes
This commit is contained in:
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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")
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
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