integration-lead-pulse
Weaver (MCP Integration Specialist) scheduled pulse check — MCP fleet health audit, integration fixes, PULSE.md + daily-digest logging, and compact Discord report delivery.
SKILL.mdskills/devops/weaver-pulse/SKILL.md
GitHubWeaver Pulse (MCP Integration Specialist)
Scheduled cron pulse for the Weaver profile. Every cycle: gate on quiet hours, sweep the MCP fleet, do ONE meaningful integration action, log to PULSE.md + daily digest, deliver a compact Discord report.
Execution Sequence
1. Quiet-hours gate (do this FIRST)
TZ='EST5EDT,M3.2.0/2,M11.1.0/2' date +%H
- Hour 00–06 → respond exactly
[SILENT], skip all work. - Hour 07+ → proceed with the pulse. Use
datefor the report timestamp (ET) anddate -ufor the PULSE.md header (UTC).
2. Fleet health sweep
Run live heartbeats through the MCP tools themselves (they prove server + transport both work), plus a port sweep for non-tool servers:
- Live tool heartbeats: BizDev
bizdev_heartbeat, Job Agentheartbeat, Git Stars / Personal Intelhealth_check, Postgrespg_execute_query, Firefoxhealth(orfirefox_health), trading-signalsmarket_summary, gbrainget_health(trackpage_count,orphan_pages,brain_score— every PULSE.md entry historically records these). - Port sweep:
netstat -ano | grep LISTENING | grep -E ":(3000|3002|5432|8501|8888|33000|9223|11434)\s"— 3000 Buzz relay, 3002 Cal.diy, 5432 Postgres native, 8501 AI Scientist, 8888 Open Coscientist, 33000 Job Agent, 9223 Firefox BiDi, 11434 Ollama. - Identify unknown/contested listeners:
tasklist //FI "PID eq <pid>"silently returns EMPTY in git-bash — usepowershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter 'ProcessId=<pid>' | Select-Object ProcessId,Name,CommandLine | Format-List"for the owning process + full command line, andcurl -s -m 5 http://localhost:<port>/(response body) to fingerprint the service. A port can carry BOTH a docker-proxy listener (PID = com.docker.backend/wslrelay) and a native process — enumerate every PID netstat shows for the port, don't stop at the first. - Docker:
docker ps --format '{{.Names}} {{.Status}}'for container health. Cross-check container start times againstcom.docker.backend/wslrelayprocess start times: identicalUp X minutesacross the fleet + a fresh Docker backend start = a daemon restart, not N independent container crashes. After a daemon restart, GPU compose stacks (e.g.yt-anim-ollama,yt-anim-comfyui,yt-anim-fishspeech) commonly exit (255) and DON'T auto-restart — re-probe their ports fresh instead of carrying a stale "down" flag forward. A container can report(healthy)while the app still returns curl 000 (healthcheck passed, app not yet serving) — re-probe once more before flagging it down. - Git activity:
find . -maxdepth 2 -name .git | xargs git log --since="24 hours ago"scan over the github root (seerecurring-status-checksfor large-repo fallbacks). ⚠️ MSYS absolute-path false-zero:find ${MY_REPOS}/...+git -C "$d"with absolute MSYS paths SILENTLY returns zero commits even when repos are active — the scan is lying, not the repos. On a zero result, re-verify from the github root with relative paths (cd ${MY_REPOS} && for r in _project website-landlord ghl hermes-config; do git -C "$r" log --oneline --since='48 hours ago' | wc -l; done). Live example Aug 7 2026: absolute scan 0 commits, relative scan ghl 17 + website-landlord 4 + hermes-config 1.
3. One meaningful integration action
Pick ONE concrete fix/discovery per cycle (see Pitfalls for the recurring ones). Verify end-to-end before reporting; do not just describe intent.
Quiet-cycle fallback — gbrain orphan cross-linking (cheap, deterministic, clears chronic PULSE.md Next Actions): if the fleet is healthy and no fix is pending, run mcp__gbrain__find_orphans and cross-link orphan pulse pages to their chronological predecessors with precedes edges (link_source: integration-lead-pulse). Verified Aug 7 2026: 2 orphans → 0, brain score 83 → 85 in 4 tool calls. Full sequence + slug-naming/temporal notes: references/gbrain-orphan-crosslinking.md.
4. Logging
- PULSE.md (
${USER_HOME}\AppData\Local\hermes\profiles\integration-lead\PULSE.md) — append with a quoted heredoc (zero truncation risk, no read-back needed):
TS=$(date -u +%Y-%m-%d\ %H:%M)
cat >> PULSE.md <<'EOF'
## Pulse @ $TS UTC
- **Status**: 🟢 Nominal / 🟡 Attention Needed / 🔴 Issue Found
- **Focus**: [what you investigated this cycle]
- **Findings**: [specific and actionable]
- **Next Action**: [one thing to do next]
EOF
- Daily digest:
python "${MY_REPOS}/Documents/github/_project/scripts/append-digest.py" "Weaver Pulse" "- finding1\n- finding2"— use the literalE:/...Windows path (MSYS/e/...breaks cross-drive Python calls). Check the digest file exists before assuming it was written. The script prints[SILENT]+Quiet hours (00:00-06:59 EST) — saved to digest onlywhen its OWN clock check fires (observed at 21:24 ET = 01:24 UTC — it keys on UTC, not ET). That output is the script's internal digest-mode message, NOT a directive to suppress the report. The step-1 ET gate is the ONLY authority on[SILENT]delivery. If the script prints it during non-quiet ET hours, the digest WAS still appended — confirm via the file/echo, then deliver normally.
5. Report
Follow discord-report-format: emoji header + ET timestamp, compact bullets, box-drawing separators, 🔍 Checked: footer. Keep under 1500 chars. Start with a notice line if any listed skill was skipped.
Pitfalls (learned the hard way)
@henkey/postgres-mcp-serverignoresDATABASE_URL— its tools errorNo connection string providedunlessPOSTGRES_CONNECTION_STRINGis in the server'senv:block. Configs commonly carry a staleDATABASE_URL(asyncpg scheme → dead port/instance that rejects creds) that the server never reads. Live-verify before wiring: pass the candidate string as the per-callconnectionStringtool arg and runSELECT datname FROM pg_database— success proves the exact value; then commit it to configenv:(activates on next restart).enabled: falsein config is the real blocker, not the restart. A server that never comes up across many pulses may be disabled in config. Grep the config entry (grep -A6 "<server>:" config.yaml) forenabled: falsebefore scheduling yet another "restart overdue" action. (AI Scientist was mislabeled restart-blocked for 16 days; the actual cause wasenabled: false.)- Firefox MCP bridge: registered tools ≠ functional bridge; port up ≠ bridge connected (Aug 8 2026).
ultimate-firefox-mcptools appeared in-session for the first time ever, butlist_tabsfailedCannot detect Firefox: No Firefox found on BiDi:9239 or CDP:9239— a port mismatch, not a crash. Configargs --port 9239≠ live headless Firefox on 9223 (--headless --no-remote --profile <hermes-profile> --remote-debugging-port 9223). ALWAYS confirm registration with a read-only tool call (list_tabs); when a bridge can't find its browser: (1)netstat -ano | grep LISTENING | grep -E ":(9223|9239)\s"for the real listener; (2)powershell Get-CimInstance Win32_Process -Filter "ProcessId=<pid>"for the actual--remote-debugging-port; (3) enumerate ALL non-contentproc Firefox cmdlines (Where-Object { $_.CommandLine -notmatch 'contentproc' }) — two headless instances on the same profile+port = profile-lock risk, kill the loser withtaskkill //F //PID; (4) BiDi liveness:POST /session→400 The handshake request must use GET methodis spec-CORRECT alive (WebDriver BiDi handshake is a GET WebSocket-upgrade),/json/version404 = BiDi-only not CDP — don't misread either as dead. Fix: align bridge--portto the live listener (or relaunch browser on the configured port), dedupe instances, restart Hermes. Full recipe:references/firefox-bidi-bridge-diagnostics.md. Structural root cause found (Aug 10 2026): the bridge was broken INDEPENDENTLY of the port mismatch — (1)_check_bidicalledwebsockets.connect(..., timeout=)but websockets 15.0.1 renamed the kwarg toopen_timeout→ TypeError → silent False → "Cannot detect Firefox" forever (CDP fallback also dead: Firefox 129+ removed/json/version); (2) Firefox returnschildren: null, andctx.get("children", [])yields None →for child in Nonecrash in the flatten helper. Fixedopen_timeout=timeout(main.py) +(ctx.get("children") or [])(tools/init.py), then relaunched a fresh headless Firefox. VERIFIED WORKING end-to-end (Aug 11 2026) — first-ever full round-trip. Canonical proof pattern:list_tabs(baseline) →navigate(url, wait="complete")(returnsnavigation_id) →list_tabsagain and confirm the tab's URL updated. If those three calls round-trip, the bridge is production-ready for browse/QA workflows. Do NOT treatcurl http://localhost:9223→ 000 as a down signal — BiDi is a WebSocket GET-upgrade, not HTTP; the bridge liveness check is the tool call itself. - trading-signals
N/A/ openbb-finance errors = shared OpenBB install version skew, FIXED via durable venv (Aug 1 2026). Root cause: openbb 4.7.x meta-package's generatedpackage/equity.pystatically importsOBBject_EquityInfofromopenbb_core.app.provider_interface, but openbb-core 1.6.13 (latest) only createsOBBject_*types DYNAMICALLY viaProviderInterface._generate_return_annotations()(181 models incl. EquityInfo) and never injects them into the module namespace — so the import fails and every OpenBB-dependent server (trading-signals, openbb-finance) breaks at the same line. Verified byte-identical to official wheel + sdist; no module__getattr__anywhere. Downgrading openbb 4.7.2→4.7.1→4.6.0 did NOT help on the system (build regenerates the same broken import) — but a clean venv with openbb 4.6.0 + openbb-core 1.6.13 works (system is polluted by editable.pthfinders pre-registering extensions before auto_build). Fix applied: durable venv${USER_HOME}\AppData\Local\hermes\venvs\openbb-trading(openbb 4.6.0 + mcp 1.28.1), rewiredtrading-signals+openbb-financecommand:to that venv's python in root config. Verified: server module loads all 4 tools, live SPY quote works. Full recipe:references/shared-dependency-version-skew.md. - trading-signals
market-summaryN/A survived the venv fix — second root cause in the server code itself (Aug 2 2026). Two independent bugs intrading-signals-mcp.py, independent of the version skew above: (1)obb.equity.price.quote(sym)with NO provider → OpenBB default provider routing hangs without API keys; fixprovider="yfinance". (2) extraction assumed row dicts, but openbb-core 1.6.13OBBject.to_dict()returns COLUMN-oriented dicts ({'bid': [744.11], ...}— list values, noprice/close/ckeys) → every symbol silently resolves to "N/A" even on successful quotes. Fix: unwrap{k: (v[0] if isinstance(v, list) and v else v) for k, v in data.items()}, fall back tobid, compute change% fromprev_close. Verified in-process: SPY 744.11 live, all 5 symbols numeric. - trading-signals
market-summary0.0/−100% regression (Aug 3 2026) — thebidfallback was itself a third bug. yfinance quoteto_dict()has NOprice/close/ckey at all;bid/askare literally0.0outside trading hours (pre-market/overnight). The Aug 2 fallbackdata.get("price", data.get("bid", ...))resolved tobid=0.0→ change% computed(0−prev)/prev = −100%for SPY/DIA/IWM/VXX (QQQ only "worked" because yfinance had populated its bid). Durable fix: price fallback chainlast_price → price → open → close/c → nonzero bid/ask(skip zero values), and never compute change% whenpriceis falsy.openis reliably populated pre-market. Committed890da31. Full detail + rawto_dict()evidence:references/trading-signals-data-shape-fixes.md. - Audit EVERY tool in a file after a data-shape fix.
technical_signalshad the identical column-oriented-to_dict()bug that the Aug 2 fix addressed only inmarket_summary— it still erroredUnexpected data format: dictbecausehistorical().to_dict()is also column-oriented ({"close": [...]}) while the code only handled list-of-dicts. Fix: extract theclose/ccolumn directly from the dict form. When a shape quirk hits one tool, grep the same file for otherto_dict()/data.getconsumers. - MACD signal-line unit mismatch (Aug 4 2026) —
MACD_BULLISHunreachable, fixed + committed9113836.signal_line = sum(closes[-9:]) / 9was SMA of the last 9 closes (price units, ~$770) whilemacd = ema12 − ema26is ~2–3 units →macd > signal_linewas always false → the tool could only ever printMACD_BEARISH. Second latent bug:ema12/ema26were simple averages of the last 12/26 closes, not true EMAs. Fix: full EMA-12/26 series over all closes, thensignal_line = EMA-9 of the MACD series(standard MACD(12,26,9)). Verified live: SPY macd 2.56 / signal 0.56 / hist 2.0 →MACD_BULLISHfires for the first time. General lesson: a one-sided indicator comparison usually means unit mismatch (indicator scale vs price scale) — verify BOTH branches are reachable, not just the one live data shows. Branch-coverage recipe + diff:references/trading-signals-macd-fix.md. Existingscripts/verify-trading-signals.pystill covers Bugs 1–3 (compile, index prices, technical_signals shape); add the MACD unit-scale + branch checks from the reference if you touch MACD again. - A patched MCP server file does NOT go live until its subprocess restarts — the running process holds the old module in memory, so live tool calls still show old behavior after a correct on-disk fix (trading-signals ran stale code for hours; fresh-import of the file proved the code was right). Verify fixes against a FRESH import of the on-disk file, report "goes live on next restart", don't misdiagnose the live server. After config rewiring to a new venv, check for duplicate server PIDs (stale subprocess on the old python survives alongside the new one — only ONE config entry exists).
- Verify MCP fixes with a focused assertion script, not just eyeballed output. Reusable ad-hoc verifier:
scripts/verify-trading-signals.py(asserts py_compile clean; every index price > 0 and change ≠ −100.0;technical_signals≥ 20 points with no dict-format error; exits non-zero on failure). Run with the venv python andenv -u PYTHONPATH— the agent process PYTHONPATH carries Python-3.13 ABI packages that break 3.11 venv servers. Place under the OS temp dir with ahermes-verify-prefix when writing ad-hoc variants; clean up after. openbb-tradingvenv can break silently between cycles (importlib_metadatamissing, Aug 4 2026) —import openbbfailedModuleNotFoundError: No module named 'importlib_metadata'atopenbb_core/app/extension_loader.py(line 8), so trading-signals live tools erroredOpenBB not installedwhile openbb-finance (spawned earlier, openbb imported at module load) kept working — the mismatch is the diagnostic clue. Fix:pip install importlib-metadatainto the venv (${USER_HOME}/AppData/Local/hermes/venvs/openbb-trading/Scripts/python.exe -m pip install importlib-metadata). Self-heal property: trading-signals tool functions lazy-import openbb INSIDE each call, so after fixing the venv on disk the RUNNING server recovers on the next call — no Hermes restart needed. Verify with livemarket_summary+technical_signalsMCP calls, not just in-process imports.- REMAINING (trading-signals): OpenBB tool calls hang inside the FastMCP worker-thread context. Not reproduced Aug 3 — the live server returned
market-summary/technical-signalsquickly (wrong values, not hangs), so the hang may be resolved by the venv rewiring or only manifest post-restart with the patched code. Re-check after the next Hermes restart.market-summaryANDtechnical-signalsover stdio MCP hang indefinitely (orMcpError: Connection closed— server process dies mid-call) while identical code runs 2–5s in the main thread. Isolation technique: load the module withimportlib.util.spec_from_file_locationand call the tool function directly in the main thread — fast JSON ⇒ data layer fine, hang is theanyio.to_threadcontext; suspected openbb-core extension auto-build thread-safety. Debug next: pre-warm OpenBB at module import (main thread, beforemcp.run()), or wrap tool bodies in a dedicated thread; escalate to Forge with repro if confirmed. Full case study:mcp-endpoint-testing→references/fastmcp-thread-context-hang.md. - Clean-venv control test before cycling versions: when a Python MCP server's tool calls fail with
N/A/ImportErrorfrom a shared library, build a throwaway venv (python -m venv C:/tmp/testvenv— Windows path!), install the SAME versions, rerun the import. If the venv works, the system install is polluted — don't downgrade blindly, isolate the server in a durable per-server venv instead and pointcommand:at it. - Ghost-package quirk in fresh venvs: right after
python -m venv,pip listmay show a package (e.g.mcp) thatimportcannot find — pip resolved it from the base interpreter's metadata. Fix:pip install --force-reinstall --no-deps <pkg>==<ver>inside the venv, then re-verify the import. - Windows venv creation needs
C:/...paths:python -m venv /c/tmp/foo(MSYS path) silently creates nothing; useC:/tmp/foo. Same forpip download -d— MSYS/c/...args get mangled; passC:/.... - Long-running known-down state (MemPalace 3001, AI Scientist 8501): reference previous PULSE.md entries for the known cause instead of re-diagnosing from scratch every cycle. Note whether it moved, don't re-litigate.
- Config edits are write-blocked: edit
config.yamlvia a small Python script executed throughterminal()(read → assert unique anchor → replace → verify → clean up), notwrite_filedirectly. - Decommissioned compose project → orphaned container-name squatting (Aug 7 2026). A compose project whose dir was emptied/deleted can leave containers behind, still squatting on
container_name:values the ACTIVE stack's compose needs.docker compose up -dfrom the active dir silently does nothing for those services, and the dependent app runs degraded with no job engine (postiz app pointed atTEMPORAL_ADDRESS=temporal:7233; notemporalserver container existed — only Exited orphans from deletedGradientRun/postizwhose compose dir was empty). Detect:docker compose lsshows a project whose CONFIG FILES dir is empty;docker inspect <container> --format '{{index .Config.Labels "com.docker.compose.project"}} | {{index .Config.Labels "com.docker.compose.project.working_dir"}} | {{index .Config.Labels "com.docker.compose.project.config_files"}}'reveals the squatter's true origin; grep the active compose for the samecontainer_name:. Fix:docker rmonly the squatter containers (NOTdocker compose down— that takes the running app down), thencd /path/to/active && docker compose up -d <engine> <admin-tools>, thendocker update --restart unless-stoppedon the new containers (orphans typically hadrestart=no— the exact reason they stayed dead after a daemon restart). Verify beyond "Up": service port open, query the engine from its admin container (docker exec <admin-tools> tctl --address <engine>:7233 namespace list— namespaces registered = serving),docker exec <app> getent hosts <engine-host>, app logs clean. Hermes terminal guard blocksdocker compose up -din foreground ("appears to start a long-lived server/watch process") — run viaterminal(background=true)thenprocess(action='wait');docker rmalone runs fine in foreground, so split the steps.
Verification
- Quiet-hours gate ran (no report during 00–06 ET)
- Live heartbeats + port sweep + docker + git scan all performed
- PULSE.md entry appended (verify with
tail) - Daily digest appended (verify script output)
- If trading-signals touched:
scripts/verify-trading-signals.pypasses (venv python +env -u PYTHONPATH, exit 0) - Report ≤1500 chars, discord-report-format compliant, 🔍 Checked: timestamp present
Related Skills
mcp-fleet-audit— full fleet audit sequence, dormant-server discovery, standalone verificationnative-mcp— MCP client config reference and troubleshootingdiscord-report-format— delivery formatting rulesrecurring-status-checks— escalation tracking, append-log safety, large-repo git scan fallbacks