cron-job-management
Hermes cron job lifecycle management — creation, model configuration, manual testing, and execution monitoring.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Hermes cron job lifecycle management — creation, model configuration, manual testing, and execution monitoring.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Comprehensive wiki maintenance: daily structural health checks (index reconciliation, log separator fixes, pipeline watchdog alerts), comparison page updates (adding items to multi-section comparison tables), and page relocation (moving/renaming pages while maintaining link integrity).
Pre-flight checklist and procedures for archiving, deleting, or migrating Hermes skills. Prevents accidental removal of cron-referenced skills. Covers the 3-layer skill structure and config.yaml management. Includes skill inventory management, promotion workflows, and archival conventions.
Karpathy's LLM Wiki: build/query interlinked markdown KB.
Deep analysis of blog authors' recent thoughts, philosophy, and positions. Goes beyond entity page creation to extract cited ideological positions, track thought evolution, and enable ongoing RSS monitoring for thought updates.
Query the blogwatcher-cli SQLite database for RSS scan results. Use pre-verified column names and query templates to avoid errors.
Synthesize perspectives from multiple opinion leaders on a shared topic into a cross-referenced wiki index page. Use when the user wants to consolidate different viewpoints on the same concept.
| name | cron-job-management |
| category | productivity |
| description | Hermes cron job lifecycle management — creation, model configuration, manual testing, and execution monitoring. |
cronjob(
action="create",
prompt="<self-contained prompt>",
schedule="0 19 * * *", # cron expression or '30m', 'every 2h', etc.
name="descriptive-name",
skills=["skill-name"], # optional: pre-load skills
enabled_toolsets=["web", "file", "terminal"], # restrict tools to what's needed
model={"provider": "deepseek", "model": "deepseek-v4-flash"}, # optional: pin model
deliver="discord:1233771389367095377:1491801814222504169", # where to send results
script="optional_pre_run_script.py", # optional: runs before job prompt
)
Key rules:
prompt must be fully self-contained — cron runs have no current-chat contextkzinmr/ai-topics at ~/ai-topics, NOT NousResearch/ai-topics." Common pitfall: AI may infer incorrect repo names/URLs from training data if not explicitly told.skills are provided, the future cron run loads them in order, then follows the promptenabled_toolsets restricts which tools the job's agent can use — reduces token overheadAlways specify both provider and model explicitly:
cronjob(
action="update",
job_id="<job_id>",
model={"provider": "deepseek", "model": "deepseek-v4-flash"},
)
Pitfall: If only model is set without provider, the provider may default to custom which could use the wrong API endpoint. Always set both.
Route one cron job's output to a different platform without running a second LLM.
Source job (the one doing the work) runs on its schedule and delivers to its primary platform:
deliver="slack:C077ACXR5UY"
Relay job is a no_agent=True job with a small Python script that reads the source job's latest output file and prints its delivered content to stdout:
cronjob(
action="update",
job_id="<relay_job_id>",
no_agent=True,
script="relay_script.py", # ~/.hermes/scripts/relay_script.py
schedule="45 0,4,8,12,16,20 * * *", # stagger after source job
deliver="discord:guild:thread",
skills=[], # no skills needed — no LLM invocation
)
Relay script (~/.hermes/scripts/relay_script.py):
import re, os, sys
from pathlib import Path
hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
source_output = hermes_home / "cron" / "output" / "<source_job_id>"
files = sorted(source_output.glob("*.md"), reverse=True)
if not files:
sys.exit(0)
content = files[0].read_text(encoding="utf-8", errors="replace")
# Extract the agent's delivered response section
match = re.search(r"^## Response\s*\n(.*)", content, re.MULTILINE | re.DOTALL)
if match:
print(match.group(1).strip())
## Response ExtractionWhen extracting content after a heading with re.MULTILINE | re.DOTALL:
# WRONG — non-greedy .*? + $ with MULTILINE matches only ONE line:
re.search(r"^## Response\s*\n(.*?)$", content, re.MULTILINE | re.DOTALL)
# RIGHT — greedy .* without $ captures everything to end:
re.search(r"^## Response\s*\n(.*)", content, re.MULTILINE | re.DOTALL)
Why: $ with re.MULTILINE matches end of any line (not end of string). Non-greedy .*? stops at the first possible end-of-line. Greedy .* with DOTALL captures everything to end of string.
sys.exit(0) produces no deliverycronjob(action="run", job_id="<job_id>")
cronjob(action="list") # check last_status, last_run_at, last_delivery_error
process(action="list") # check for active subprocess sessions
~/.hermes/cron/data/~/.hermes/cron/output/Important: cronjob action='run' queues the job for execution in a separate session. Results are delivered asynchronously to the deliver target (Discord channel, Telegram, etc.), NOT returned immediately to the current conversation.
For monitoring multi-stage cron pipelines with auto-healing capabilities. Full pattern documented in references/watchdog-pipeline.md.
Quick reference:
Auto-fixable patterns: pipe table corruption, line number prefix pollution, index duplicates, count mismatches, missing separators. Never auto-fix broken wikilinks or delete orphan pages.
# Check job status
cronjob(action="list")
# Check active subprocesses
process(action="list")
# Poll a specific process for output
process(action="poll", session_id="<session_id>")
# Wait for process completion
process(action="wait", session_id="<session_id>", timeout=300)
Common issues:
jobs.json format: The file is {"jobs": [...], "updated_at": "..."} with each job using "id" (not "job_id"). Access via data["jobs"] not bare iteration.last_delivery_error: "no delivery target resolved for deliver=None" — job ran successfully but had nowhere to send results. Set deliver to a valid target.list but process list shows nothing — the subagent may have already completed and delivered.enabled_toolsets to reduce overhead, or split into smaller batches.jobs.json may differ from what was actually used last run. The job's prompt can be updated by the cron system via skill injection, script injection, or context enrichment between runs. Always check the latest output file (~/.hermes/cron/output/<job_id>/latest.md) in the ## Prompt section to see what the job actually ran with — not just the jobs.json definition. The output file is the source of truth for what was delivered to the agent.0 17, 10 17, 25 17) without context_from or actual data dependencies, slow upstream jobs (>60 min) can displace the entire chain. A downstream job that checks for upstream output at its scheduled start time will find nothing (or yesterday's stale output) if the upstream job hasn't finished. Detection: compare last_run_at timestamps across the chain — if a downstream job ran BEFORE an upstream job finished, you have a race. Fix: widen schedule gaps based on observed worst-case run times, or implement context_from with proper data flow. Example from 2026-05-13: wiki-health (74 min) → wiki-health-plan (68 min) → wiki-health-fix ran before plan completed, skipped all auto-fix actions. Fixed by moving wiki-health-fix from 25 17 to 50 17.last_status: error and the output file shows Status: BLOCKED with a threat pattern like exfil_curl, the assembled prompt (user prompt + loaded skill content) tripped the _CRON_THREAT_PATTERNS check. Debugging pitfall: read_file masks $VAR_NAME patterns as *** — use od -c or execute_code for hex inspection. See [[references/cron-injection-scanner]] for full pattern list, debugging workflow, and fix recipes.script parameter)When a cron job uses script="name.py" (resolved from ~/.hermes/scripts/), the script generates JSON context that gets injected into the job prompt before each run.
Adjusting parameters (time ranges, limits, thresholds):
~/.hermes/scripts/<name>.pytimedelta(days=X) or limit=N values in the relevant functionRemoving expensive or unnecessary data sources:
main() payload dicttrending_report() call that runs a heavy subprocessAdding new data collection functions:
list[dict[str, str]] or similar structured datarepo: Path + optional args)related_wiki_pages() that parses [[wikilink]] patterns for connected contentTo implement "for each recent page, find connected pages":
1. Parse outgoing [[links]] from the page content (re.compile(r'\\[\\[([^\\]|]+)(?:\\|[^\\]]+)?\\]\\]'))
2. Search for backlinks via `grep -rl "\\[\\[slug(\\||\\])" wiki/`
3. Deduplicate against recent pages, cap per source and total
4. Build excerpts for the result list
When a cron reporting job produces duplicate/semantically-overlapping content across
adjacent slots, filter candidates in the pre-run script rather than relying on
LLM prompt guidance ("avoid repeating"). Soft dedup fails because the LLM sees
the same candidate pool and often re-picks the same topics.
avoid_repeating_recent_posts guidance is not respectedTrack previous outputs — the script must have access to what was already reported. For Slack/Discord relay pipelines, fetch recent bot posts via API and extract their wikilinks/slugs/topic identifiers.
Build a "covered" set — collect identifiers from the most recent N posts (2-3 is usually enough to prevent adjacent-slot duplicates).
def _get_covered_slugs(recently_covered: list[dict], n_posts: int = 2) -> set[str]:
slugs: set[str] = set()
for post in recently_covered[:n_posts]:
for wikilink in post.get("wikilinks", []):
slugs.add(Path(wikilink).stem)
return slugs
Filter the candidate pool — remove pages/topics whose identifiers match the covered set BEFORE passing data to the LLM.
def _dedup_filter(pages: list[dict], covered_slugs: set[str]) -> list[dict]:
return [p for p in pages
if Path(p["path"]).stem not in covered_slugs]
Fallback when exhausted — if filtering leaves too few candidates (e.g., < 3), skip dedup entirely and include a warning in the payload. Never pass an empty candidate pool — the LLM will hallucinate.
if len(deduped_candidates) >= 3:
candidates = deduped_candidates
dedup_applied = True
# else: fall through with unfiltered candidates
Surface dedup in payload — add transparency fields so the LLM knows what was filtered and why:
"dedup": {
"recently_covered_topics": [...],
"hard_filter_applied": true,
"hard_filter_removed_slugs": ["areal", "hybrid-flow", "slime-rl"]
}
The ai-topics-slack-hot-posts job at 5e91a0b47c32 runs every 4 hours (0,4,8,12,16,20
UTC; 9,13,17,21,1,5 JST). The ai-topics-discord-hot-posts job (56548a0ed1bf) relays
Slack output to Discord verbatim 15 min later.
Problem: 05:30 JST (20:30 UTC, "pre-morning") and 09:30 JST (00:30 UTC, "morning") reported the same RL training library topic because 12 wiki pages were created in one batch and the soft dedup prompt was ignored.
Fix: Modified ai_topics_slack_hot_posts_context.py to filter recent_wiki_pages
and related_wiki_pages against wikilinks from the last 2 bot posts. Since the Discord
relay is a no_agent passthrough, fixing the source script fixes both channels.
Result: The 09:30 report can no longer pick topics already covered at 05:30.
recently_covered_topics list will be empty and filtering
is a no-op. Don't crash the script.pi can be ambiguous (Pinecone vs Pi agent).
For entity pages, cross-reference by reading the page's title, not just the slug.
For dedup purposes, slug-based filtering is usually sufficient since adjacent-slot
duplicates are the exact same wiki pages.# Run the script directly to verify JSON output
python3 ~/.hermes/scripts/<name>.py 2>&1 | head -50
# Check for syntax errors
python3 -c "import ast; ast.parse(open('~/.hermes/scripts/<name>.py').read()); print('OK')"
# Verify git repo changes if the script exists in both locations
diff ~/.hermes/scripts/<name>.py ~/ai-topics/scripts/<name>.py # sync if different
Location notes:
~/.hermes/scripts/ (the canonical location)~/ai-topics/scripts/ — check and sync if needed"script": "ai_topics_slack_hot_posts_context.py")cronjob(action="pause", job_id="<job_id>")
cronjob(action="resume", job_id="<job_id>")
cronjob(action="remove", job_id="<job_id>")
Cron schedules use UTC. JST = UTC + 9 hours.
"0 19 * * *""0 0 * * *"Derive filtering criteria from an opinion source and batch-update cron job prompts to inject selection bias.
SKIP / DEPRIORITIZE (0.3x weight):
- [specific frameworks, patterns] — [why to skip]
COMPOUND (3x weight — focus here):
- [specific patterns] — [why to boost]
WAIT / OBSERVE:
- [items to defer until validated]
## Karpathy's Filter (APPLY THIS)
[filter content]
Skip: AutoGen/AG2, CrewAI, autonomous agent pitches, agent app stores, SWE-bench chasing, Semantic Kernel, DSPy, horizontal agent platforms, per-seat SaaS pricing Compound: Context engineering, tool design, orchestrator-subagent pattern, eval discipline, harness mindset, MCP, practical production deployment
Extract and analyze data from cron job output files.
Cron job outputs: ~/.hermes/cron/output/<job_id>/YYYY-MM-DD_HH-MM-SS.md
Each file has three sections:
Parse markdown tables in cron reports:
import re
rows = re.findall(r'\|\\s*\\d{4}-\\d{2}-\\d{2}\\s*\\|\\s*(.+?)\\s*\\|\\s*(.+?)\\s*\\|\\s*[\\d,]+\\s*\\|', content)
Multiple consecutive runs may return [SILENT] (no new data). The last non-SILENT report is the most recent actual processing.
/opt/data/bin/xurl → /opt/data/.hermes/bin/xurlModuleNotFoundError → the script's shebang uses system python but deps are in the venv. Full debugging workflow in references/debugging-no-agent-scripts.md. Quick check: /opt/data/.hermes/venv/bin/python3 ~/.hermes/scripts/<name>.pycronjob(action='list') # find job_id by name
Full pipeline for tracking LLM token usage and API costs.
Insert at the top of the job's prompt:
# TOKEN USAGE TRACKING
At the end of your response, print exactly one line:
COST_REPORT: job=<job_name> status=ok input_tokens=0 output_tokens=0 cost=0.0
Create 4 types: daily, weekly, monthly, trend
Uses ~/.hermes/scripts/token_usage_collect.py with the appropriate argument.
SQLite3 at ~/.hermes/cron/data/token_usage/token_usage.db
Scans ~/.hermes/cron/output/<job_id>/ for COST_REPORT: lines
Deduplicates via ingested_files table.
deliver: null (shell-only)qwen36-fast default with minimal LLM usageThe token_usage_collect.py parser uses key-value extraction (parse_cost_report()) but fails silently on most real-world COST_REPORT formats. The DB shows input_tokens=0 output_tokens=0 cost=0.0 for ~80% of ingested lines. Only the strict key=value format (with no pipes, ~ prefixes, or free-text) gets parsed correctly.
Formats that BREAK the parser (silently produce 0):
COST_REPORT: job=blog-triage | input_tokens=~180K | output_tokens=~4K | model=deepseek-v4-flashCOST_REPORT: job=blog-ingest | total_tokens=45000 (uses total_tokens not input_tokens/output_tokens)COST_REPORT: job=trending-topics | model=deepseek-v4-flash | tokens_in=~85K | tokens_out=~5.5K | cost_estimate=$0.045COST_REPORT: job=blog-ingest | 17 tool calls (3 web_extract, 8 search_files...) | 2 new concept pages...COST_REPORT: job=active-crawl | pages_created=5 | raw_articles=5 (no token fields at all)COST_REPORT: job=blog-triage | tokens=52000 (uses bare tokens key)Format that DOES work:
COST_REPORT: job=blog-triage input_tokens=0 output_tokens=0 (space-separated, no pipes, no ~)Impact: summary.json and token_usage.db report total costs of ~$0.002/month when real costs are ~$87/month. The trend data is useless.
Fix path: Either (a) enforce a single COST_REPORT format across all cron job prompts, or (b) upgrade parse_cost_report() to handle the full variety of formats actually produced. See references/cost-estimation-manual.md for the manual estimation methodology used as a fallback.
When the automated DB is unreliable, use the methodology in references/cost-estimation-manual.md:
cronjob list → get all jobs with schedules, models, providersgrep -r 'COST_REPORT' ~/.hermes/cron/output/ → sample recent per-run costsMonthly review of ~/.hermes/cron/data/token_usage/summary.json
Flag jobs exceeding $5/month in Discord.
Consider migrating to cheaper alternatives if one provider dominates.