spend-alert-bridge
Diagnose why a Spend Alert (GitHub Actions MTD/7-day breach, MCP Agent Mail relay,
- License 许可
- MIT
- 最近更新
- 2026-11-11
- Stars
- ★ 6
- Forks
- 0
01 前言 Front Matter
- name
- spend-alert-bridge
- description
- Diagnose why a Spend Alert (GitHub Actions MTD/7-day breach, MCP Agent Mail relay,
02 资源文件 3
- 参考文档references/comment-router-runaway-2026-07-23.md9.9 KB
- 脚本scripts/comment-audit.py3.8 KB
- 脚本scripts/gh-cost-audit.py3.3 KB
03 SKILL.md 正文
Spend Alert Reply Bridge
Failure pattern
The spend-alert-daily.sh launchd script posts a Slack alert to #worldai-alerts (C0BCVG4F560) when GitHub Actions daily Δ or 7-day rolling exceeds its threshold. The user sees this alert re-routed to their session context via MCP Agent Mail (a separate relay process, com.mcp.agent.mail, PID ~1151), but nothing in the dropped-thread-watcher pipeline watches Agent Mail. Combined result: the alert appears in human-facing chat but there is no hook that pings me to reply in-thread.
Three distinct failure modes, all recurring:
Mode A — bridge delivery is fine, the SPEND ITSELF is the runaway (most common, 2026-07-23+)
The script posts correctly, the alert lands in #worldai-alerts, MCP Agent Mail relays it. But the user still gets a "why is spend $X" message because the alert numbers are real and trace to a runaway workflow. The bridge isn't broken — the cost is real.
Important nuance: the script's "MTD delta" is NOT a true daily-spend metric. GH's billing API returns monthly buckets only (date == "2026-07-01T00:00:00Z" for all of July). The script subtracts two snapshots taken 24h apart to estimate daily burn — this conflates real-time burn with billing-API lag. When the API finalizes a batch overnight, the next morning's delta will spike without any real new spend.
Mode A.1 — comment-router-style runaway (workflow-level noise, low minutes)
The first instinct: look at which workflow fired thousands of times. The 2026-07-23
incident was initially diagnosed as a comment-router.yml runaway (5,566 issue_comment
runs/32h, all conclusion=skipped). The comment-router's if: correctly skipped
each one, but the workflow_run event still registered.
This is real but usually a SYMPTOM, not the cause. The actual 5,566 comments were being posted by a different process — see Mode A.2.
Mode A.2 — upstream spam loop posting to issue_comment (most common runaway source, 2026-07-23+)
The recurring shape that has actually been driving the spend spikes:
- An upstream automation (typically a local daemon posting with the operator's PAT) posts the same comment to 1-3 PRs every ~1h as part of a feedback loop
- Each
issue_commentevent firescomment-router.ymlonce, which correctly skips at the job level - The workflow_run count in the GH Actions
runsAPI balloons to thousands, polluting metrics and (in the daemon's case) triggering more downstream side effects - The spend delta is mostly billing-API lag from finalized batches, not real new minutes
2026-07-23 incident (bead $USER-rouf):
- 3 PRs getting hammered:
your-project.com#8428(1,791 posts),#8420(1,773),#8421(1,770) - Total: 5,334 same-prefix daemon comments in 32h on these 3 PRs alone (vs ~6,000/48h for ALL of
comment-routertraffic) - Root cause: the daemon posted the comment BEFORE its dedup check ran, AND the dedup key used a per-tick id (a fresh bead id every tick) instead of the stable branch
- Fix: re-key dedup on the branch name AND move the dedup check before the comment write
- Full detail:
references/comment-router-runaway-2026-07-23.md
Diagnostic recipe when comment-router is the noise source:
# 1. Pull the actual comment bodies (not the workflow runs — the comments are upstream)
REPO="$GITHUB_REPOSITORY"
# NOTE: the `since` query param is ignored for `repos/<repo>/issues/comments` —
# paginate with `sort=created&direction=desc` and filter the timestamp in-process.
gh api "repos/$REPO/issues/comments?per_page=100&direction=desc&sort=updated" \
| jq -r '.[] | "\(.user.login)\t\(.author_association)\t\((.body // "")[0:80])"'
# 2. Group by author_association + author login + body prefix
# 99% of "noisy" comments will share a body prefix from one automation
gh api "repos/$REPO/issues/comments?per_page=100&direction=desc&sort=updated" \
| jq 'group_by(.author_association) | map({aa: .[0].author_association, n: length})'
# 3. Find the source — likely a local daemon, a GitHub Actions bot, or a cron
Once the source automation is identified, fix it at its own repo — BUT FIRST verify any claimed fix actually landed:
cd <source-repo>
FIX_SHA=<fix-commit>
git merge-base --is-ancestor "$FIX_SHA" origin/main && echo "merged" || echo "NOT MERGED"
# If NOT merged, the daemon is still running buggy code: merge the fix, rebuild,
# and restart its launchd job. `launchctl bootout` is a stopgap, not a fix.
# ALWAYS verify the fix is on main BEFORE telling the user "fix is shipped".
Why this matters: fixing comment-router.yml (the downstream consumer) does NOT
stop the spend alert. The fix lives at the source — the daemon that posts the comments
in the first place. The comment-router's job-level if: is already correct; it just
gets hit thousands of times because the upstream daemon is in a loop.
Mode B — set -u crash in spend-alert-daily.sh (2026-07-16, RESOLVED)
log_warn "$1" aborts on unbound variable when called with no arg during slack_post's stderr pipe under set -euo pipefail. The script terminates before slack_post ever fires. Fix landed — log_warn "${1:-}" (and same for log, log_error) per PR in jleechanorg/jleechanclaw. If you see this in tail -40 of the log, the fix has regressed.
Three independent root causes (one per mode)
| # | Mode | Cause | Fix |
|---|---|---|---|
| 1 | A.1 | comment-router.yml (PR #8354–#8355) was supposed to consolidate 4 issue_comment workflows, but its on: issue_comment trigger fires on EVERY comment, and the if: guard skips 99%+ of them. The fix only consolidated workflows; it didn't reduce trigger volume. |
Add if: github.event.comment.author_association == 'OWNER' to the on: issue_comment trigger; cap concurrency (N=5). BUT verify this isn't a downstream symptom of an upstream spam loop first (see A.2). |
| 2 | A.2 | Upstream daemon loop posts the same comment to 1-3 PRs every ~1h. Its dedup key was a per-tick id (fresh bead every tick → never matches) AND the comment posted BEFORE the dedup check ran. | (a) Move the dedup check BEFORE the comment write AND re-key dedup on the branch name (stable across ticks). (b) VERIFY the fix commit is on main HEAD before claiming the fix is shipped — git merge-base --is-ancestor $FIX_SHA HEAD. |
| 3 | B | set -u in spend-alert-daily.sh aborts on log_warn "$1" when called with no arg |
log_warn "${1:-}" everywhere, OR remove -u from the script's set line. |
The bot identity is a member of C0BCVG4F560 (verified 2026-07-23 via conversations.info returning is_member=true for the bot token fetched from ~/.bashrc HERMES_SLACK_BOT_TOKEN). The standalone bot token from HERMES_SLACK_BOT_TOKEN posts reliably; the scoped MCP identity in this session may report is_member=false because it's a different bot identity — don't conflate the two.
Recovery action when user reports a missed Spend Alert
cat $HOME/.hermes/logs/spend-alert-daily.log | tail -40— confirm whether the last run completed the Slack post or crashed on theset -uline. If it completed, the alert is in the channel — you're in Mode A (real cost spike), not Mode B (bridge broken).jq . $HOME/.hermes/state/spend-alert-state.json— check the rolling deltas match what the user reported. Ifgh_deltajumped 3x+ over baseline (e.g. $61 vs typical $15), the runway is the cause, not a billing error.curl -fsS -H "Authorization: Bearer $(bash -c 'source ~/.bashrc; echo $HERMES_SLACK_BOT_TOKEN')" "https://slack.com/api/conversations.history?channel=C0BCVG4F560&limit=5"— confirm whether the alert landed in #worldai-alerts. This is the right verification —mcp__slack__conversations_repliesmay falsely reportnot_in_channelfor the scoped identity.- If alert NOT posted (crash / silent failure): post a manual ack to #worldai-alerts using the bot token (which IS a member) — reply with the same breach numbers so the channel has the missing record.
- If alert IS posted (Mode A — the common case): reply in-thread with the diagnosis. The user wants to know WHY the spend jumped, not whether the bridge worked. Skip directly to the runaway audit below.
- Runaway audit (Mode A only): identify the repo and workflow driving the billable minutes. The pattern in 2026-07-23 was a
comment-router.ymlsymptom + an upstream daemon cause — see step 7. - Upstream spam check (Mode A.2 — the most common real cause as of 2026-07-23): before concluding the comment-router is the runaway, check who is actually posting the comments. If 99% of the
issue_commentevents have one of a small set of body prefixes (e.g. one daemon's escalation prefix), the comment-router is the victim, not the cause. The fix lives in the source that posts the comments. See Mode A.2 in the failure pattern above for the recipe. - Verify the fix is actually live before claiming resolution. If the alleged fix is a commit on a branch, run
git -C <repo> merge-base --is-ancestor <fix-sha> HEAD. If the result is "NOT MERGED", the daemon is still running buggy code. Either merge the branch or stop claiming the fix is shipped. (On 2026-07-24 a fix left unmerged on a branch produced a second spend spike identical to the first.)
Verification commands
# Step 1: script log + state
tail -40 $HOME/.hermes/logs/spend-alert-daily.log
jq . $HOME/.hermes/state/spend-alert-state.json
# Step 2: channel membership & recent posts
TOK=$(bash -c 'source ~/.bashrc; echo $HERMES_SLACK_BOT_TOKEN')
curl -fsS -H "Authorization: Bearer $TOK" \
"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200" \
| jq '.channels[] | select(.id=="C0BCVG4F560")'
# Step 3: identify the cost-driver repo
gh api "orgs/jleechanorg/settings/billing/usage" --paginate \
| jq '[.usageItems[]? | select(.product == "actions") | select(.date == "2026-07-01T00:00:00Z")] | sort_by(-.netAmount)'
# Step 4: identify the runaway workflow in that repo (last 32h)
# NOTE: `created=>` query param is treated as equality, not greater-than.
# Use `sort=created&direction=desc` and walk pages manually until the
# oldest run's `created_at` < SINCE.
REPO="jleechanorg/<repo>"
python3 ./scripts/gh-cost-audit.py # see scripts/ dir
Prevention — durable fix shape
- Mode B (script crash):
scripts/spend-alert-daily.sh: changelog_warn "$1"tolog_warn "${1:-}"(and same forlog,log_error), OR remove-ufrom the script'ssetline. Pair withscripts/tests/test_spend_alert_idempotent.py(or similar) so the crash mode is regression-tested. Status 2026-07-23: fix is in place; no crash observed today. - Mode A (runaway workflow): the recurring shape is
comment-router.ymlpatterns — workflows that listen to high-volumeissue_commentevents and use a job-levelif:guard. The job guard short-circuits before billable minutes, but the workflow still registers an event. Mitigations:- Add
if:at the workflow/job level that filters byauthor_association, comment body prefix, andissue.pull_requestBEFORE the runner is allocated - Set
concurrencygroup to a tight key (comment-router-${{ github.event.comment.id }}is already present in this file — verify it) - Audit who posts comments at 174/hr — likely a bot loop or external webhook source
- For the long term, consider an org-wide CODEOWNERS + workflow-level
if: contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)guard
- Add
- Mode A.2 (upstream daemon runaway): the recurring shape is a dedup key that uses a fresh-per-tick value (e.g.
bead_id) instead of a stable identifier (e.g.head_ref_name). Mitigations:- All dedup keys must be drawn from STABLE identifiers (branch name, PR number, commit SHA) — never from objects that get re-created every tick
- The dedup check must run BEFORE the SCM write (
comment_external,gh api repos/.../issues/.../comments) — otherwise the write is never suppressed even if the dedup would have matched - Add a
cfg.escalation_refire_secscooldown (default 3600s) so re-emission is bounded even if the dedup logic has a bug - Every fix commit must be verified as on the deployment branch before declaring "fix shipped" —
git merge-base --is-ancestor <fix-sha> <deployment-branch>. A commit on a fix branch is not a fix until that branch is merged into main AND the daemon is rebuilt AND the running launchd job is restarted.
Pitfalls
- The bridge is usually NOT broken when a Spend Alert appears in session context. MCP Agent Mail relays everything from #worldai-alerts into the session — that's working as designed. Spend the first 30 seconds confirming the Slack post landed, then pivot to the cost driver. Don't waste a turn fixing a non-broken bridge.
- "Fix is in PR #N" ≠ "fix is shipped". The skill recipes (and older incidents) often cite a PR number or branch name as if the fix is live. ALWAYS verify with
git merge-base --is-ancestor <fix-sha> HEADbefore claiming the fix is merged. The 2026-07-24 incident was caused by trusting a previous skill's stated fix location without re-verifying. - The
created=>query param ongh api repos/<repo>/actions/runsis treated as EQUALITY, not greater-than. It returns 0 hits even when plenty of runs exist after the date. Usesort=created&direction=desc+ manual pagination instead. /actions/runs/{id}/timingreturnsbillable: {}for completed runs. The billable field is reserved for in-progress / queued runs. To approximate cost, useupdated_at - created_atto compute wall-clock minutes and multiply by the runner type's $/min rate. Thegh-actions-cost-monitor.shscript does this withCOST_PER_MINUTE=0.002(Linux self-hosted rate).gh-apirepos/<repo>/issues/commentsignores thesincequery parameter. Thesincefield on the comment-schema response is also undefined. To filter comments to a time window, paginate (sort=created + direction=desc) and filter bycreated_atin-process.- The
runsAPI seesrun_duration_msas 0/null for skipped runs even if the workflow allocated a runner. Useruns.total_countto gauge trigger volume, not minutes. - Don't trust
run_duration_msfield — it returnsnullor0forskippedruns, even if the workflow was allocated a runner. Useupdated_at - created_atto get the real wall-clock time per run. - Don't assume
is_member=Falsefrommcp__slack__conversations_repliesis global to the bot — it's per-identity. TheHERMES_SLACK_BOT_TOKENenv may resolve to a different bot identity than the one bound to the live Slack MCP server. Useconversations.historywith the bot token to verify, notconversations_repliesfrom the scoped MCP. - Don't trust a
bead_idas a dedup key. Per-tick fresh objects (new bead IDs, new session IDs) make the ledger never match. Use stable identifiers (branch name, PR number, commit SHA) for dedup keys. - Don't post the user's reply via
mcp__slack__conversations_add_messagefrom this session context — it's the wrong identity. Usechat.postMessagewith the bot token from~/.bashrc. - GH billing API returns monthly buckets only —
date == "2026-07-01T00:00:00Z"is the entire month, not a day. Don't try to slice it by day; you'll see empty results. The script'sgh_deltais a 24h-difference between two MTD snapshots, NOT a true daily-spend figure. - The Agent Mail relay is in addition to, not a replacement for, the Slack post. Don't "fix" by silencing the cron — fix the crash and keep both channels.
- A "skipped" workflow run still counts.
conclusion=skippedmeans 0 billable minutes, but the workflow_run event is registered. If a workflow fires 5,000+ times in 32h, therunsAPI is polluted even if no minutes are billed. Use the runs API for diagnosing trigger volume, not the billing API. - Comment-router noise is usually a SYMPTOM, not the cause. When
comment-router.ymlfires 5,000+ times/32h onissue_comment, the first instinct is to harden the workflow (add author_association guard, lower concurrency). That fixes the symptom but misses the cause: an upstream automation (typically a PAT-driven daemon) is in a feedback loop posting the same comment to 1-3 PRs every ~1h. Pull the actual comment bodies viarepos/<repo>/issues/comments?per_page=100&direction=desc&sort=updatedand group byuser.login+bodyprefix BEFORE proposing a workflow fix. If 99% of comments come from one automation with one body prefix, the fix lives at that automation, not at the comment-router. See Mode A.2 for the fix.
Companion skills / files
references/comment-router-runaway-2026-07-23.md— original 2026-07-23 incident detailscripts/gh-cost-audit.py— runs/workflow volume audit helper (32h window, per-workflow breakdown with skip counts)scripts/comment-audit.py— issue_comments audit helper (per-author, per-prefix, per-hour breakdown for time-windowed analysis)
04 同仓库更多 Skills
instinct
Use when the user invokes /instinct or /in, or asks to "message Instinct", "ask Instinct", "tell Instinct", "check Instinct", or read replies from the configured peer assistant. iMessage first, WhatsApp Web as fallback.
hr
Compatibility alias for history-resume; produce the same fresh-state, provenance-aware resume briefing.
history-resume
Resume briefing for the current repository branch or worktree using fresh state and admitted history sources.
grok
Alias for the canonical grokbot browser conversation workflow; /grok talks to the Grok bot the same way /dot talks to Dot.
codexa-bench
Compare `codexa` and normal `codex` on a fair split of comparable work by default; use `--strict` for duplicate runs of the exact same task.
frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.