From 2ea57912f01d5929a14d155722f437f768e3062d Mon Sep 17 00:00:00 2001 From: David Elliott Date: Sat, 18 Apr 2026 04:36:01 -0700 Subject: [PATCH] cron: guide self-improve journal follow-through --- cron/scheduler.py | 28 +++++++++++++++++++++++++++- tests/cron/test_scheduler.py | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index db5991c6f..ec11f5ea9 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -91,6 +91,31 @@ _LOCK_DIR = _hermes_home / "cron" _LOCK_FILE = _LOCK_DIR / ".tick.lock" +def _build_role_prompt_prefix(job: dict) -> str: + """Return optional role-specific execution guidance for a cron job.""" + role = str(job.get("role") or "").strip().lower() + if role == "study": + return ( + "[SYSTEM: This cron job is classified as role=study. Treat it as an execution loop, not a passive summary. " + "When a run confirms a durable gap, convert that gap into explicit follow-through before you report: update the " + "owning backlog/control surface in the target repo, and if the gap is really Hermes's own capability " + "(planning, verification, delegation, evidence handling, candidate selection, or similar), also create or " + "update Hermes self-improvement work via self_improvement_pipeline or an equivalent backlog issue when those " + "tools are available. If you decide no action is warranted yet, say why in the report instead of silently " + "continuing.]" + ) + if role == "self-improve": + return ( + "[SYSTEM: This cron job is classified as role=self-improve. Treat stale or missing evidence as a concrete " + "follow-through item, not a passive observation. If self_improvement_pipeline or self_improvement_evidence_gate " + "shows journal_entries stale or missing, either refresh the owning journal/control surface with a legitimate " + "new entry when this run produced a real durable outcome, or leave the reliability floor degraded and report " + "the exact external blocker. Do not claim the issue is fixed without advancing the journal source itself, and " + "do not mask true stale evidence.]" + ) + return "" + + def _resolve_origin(job: dict) -> Optional[dict]: """Extract origin info from a job, preserving any extra routing metadata.""" origin = job.get("origin") @@ -608,7 +633,8 @@ def _build_job_prompt(job: dict) -> str: "Never combine [SILENT] with content — either report your " "findings normally, or say [SILENT] and nothing more.]\n\n" ) - prompt = cron_hint + prompt + role_prefix = _build_role_prompt_prefix(job) + prompt = cron_hint + (role_prefix + "\n\n" if role_prefix else "") + prompt if skills is None: legacy = job.get("skill") skills = [legacy] if legacy else [] diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index 2717584e4..88bc9030e 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -1174,6 +1174,26 @@ class TestBuildJobPromptSilentHint: prompt_pos = result.index("My custom prompt") assert system_pos < prompt_pos + def test_study_role_injects_execution_guidance(self): + job = {"prompt": "Study Chapter 7", "role": "study"} + result = _build_job_prompt(job) + assert "classified as role=study" in result + assert "execution loop, not a passive summary" in result + assert "self_improvement_pipeline" in result + + def test_self_improve_role_injects_journal_followthrough_guidance(self): + job = {"prompt": "Run the loop", "role": "self-improve"} + result = _build_job_prompt(job) + assert "classified as role=self-improve" in result + assert "journal_entries stale or missing" in result + assert "do not mask true stale evidence" in result + + def test_non_study_role_does_not_inject_role_guidance(self): + job = {"prompt": "Send report", "role": "report"} + result = _build_job_prompt(job) + assert "classified as role=study" not in result + assert "classified as role=self-improve" not in result + class TestBuildJobPromptMissingSkill: """Verify that a missing skill logs a warning and does not crash the job."""