Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions cron/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,9 +816,9 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> Option
delivery_content = (
f"Cronjob Response: {task_name}\n"
f"(job_id: {job_id})\n"
f"-------------\n\n"
"-------------\n\n"
f"{content}\n\n"
f"To stop or manage this job, send me a new message (e.g. \"stop reminder {task_name}\")."
"To stop or manage this job, send me a new message (e.g. \"stop reminder {task_name}\")."
)
else:
delivery_content = content
Expand Down Expand Up @@ -1220,7 +1220,7 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
path.relative_to(scripts_dir_resolved)
except ValueError:
return False, (
f"Blocked: script path resolves outside the scripts directory "
"Blocked: script path resolves outside the scripts directory "
f"({scripts_dir_resolved}): {script_path!r}"
)

Expand Down Expand Up @@ -1512,9 +1512,9 @@ def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:

if skipped:
notice = (
f"[IMPORTANT: The following skill(s) were listed for this job but could not be found "
"[IMPORTANT: The following skill(s) were listed for this job but could not be found "
f"and were skipped: {', '.join(skipped)}. "
f"Start your response with a brief notice so the user is aware, e.g.: "
"Start your response with a brief notice so the user is aware, e.g.: "
f"'⚠️ Skill(s) not found and skipped: {', '.join(skipped)}']"
)
parts.insert(0, notice)
Expand Down Expand Up @@ -1665,8 +1665,8 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
f"# Cron Job: {job_name}\n\n"
f"**Job ID:** {job_id}\n"
f"**Run Time:** {now_iso}\n"
f"**Mode:** no_agent (script)\n"
f"**Status:** script failed\n\n"
"**Mode:** no_agent (script)\n"
"**Status:** script failed\n\n"
f"{output}\n"
)
return False, doc, alert, output
Expand All @@ -1681,8 +1681,8 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
f"# Cron Job: {job_name}\n\n"
f"**Job ID:** {job_id}\n"
f"**Run Time:** {now_iso}\n"
f"**Mode:** no_agent (script)\n"
f"**Status:** silent (wakeAgent=false)\n"
"**Mode:** no_agent (script)\n"
"**Status:** silent (wakeAgent=false)\n"
)
return True, silent_doc, SILENT_MARKER, None

Expand All @@ -1692,17 +1692,17 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
f"# Cron Job: {job_name}\n\n"
f"**Job ID:** {job_id}\n"
f"**Run Time:** {now_iso}\n"
f"**Mode:** no_agent (script)\n"
f"**Status:** silent (empty output)\n"
"**Mode:** no_agent (script)\n"
"**Status:** silent (empty output)\n"
)
return True, silent_doc, SILENT_MARKER, None

doc = (
f"# Cron Job: {job_name}\n\n"
f"**Job ID:** {job_id}\n"
f"**Run Time:** {now_iso}\n"
f"**Mode:** no_agent (script)\n\n"
f"---\n\n"
"**Mode:** no_agent (script)\n\n"
"---\n\n"
f"{output}\n"
)
return True, doc, output, None
Expand Down Expand Up @@ -1761,7 +1761,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
f"# Cron Job: {job_name}\n\n"
f"**Job ID:** {job_id}\n"
f"**Run Time:** {_hermes_now().strftime('%Y-%m-%d %H:%M:%S')}\n"
f"**Status:** BLOCKED\n\n"
"**Status:** BLOCKED\n\n"
"The assembled prompt (user prompt + loaded skill content) tripped "
"the cron injection scanner and the agent was NOT run.\n\n"
f"**Scanner result:** {block_exc}\n\n"
Expand Down Expand Up @@ -1916,7 +1916,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
f"(job.model={job.get('model')!r}, "
f"HERMES_MODEL={os.getenv('HERMES_MODEL', '')!r}, "
"config.yaml model.default missing or empty). "
f"Set a per-job model via "
"Set a per-job model via "
f"`cronjob action=update job_id={job_id} model=<name>` or set a "
"default with `hermes model <name>`."
)
Expand Down Expand Up @@ -2054,12 +2054,12 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
job_id,
)
raise RuntimeError(
f"Skipped to prevent unintended spend: global inference config "
"Skipped to prevent unintended spend: global inference config "
f"drifted since this job was created ({_changes}), and this job "
f"is unpinned. No inference call was made. To run on the new "
f"config, pin it explicitly: `cronjob action=update "
"is unpinned. No inference call was made. To run on the new "
"config, pin it explicitly: `cronjob action=update "
f"job_id={job_id} provider=<provider> model=<model>` "
f"(or pin the original values to keep them). See #44585."
"(or pin the original values to keep them). See #44585."
)

fallback_model = _cfg.get("fallback_providers") or _cfg.get("fallback_model") or None
Expand Down Expand Up @@ -2265,7 +2265,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
# for delivery logic (empty response = no delivery).
logged_response = final_response if final_response else "(No response generated)"

output = f"""# Cron Job: {job_name}
output = """# Cron Job: {job_name}

**Job ID:** {job_id}
**Run Time:** {_hermes_now().strftime('%Y-%m-%d %H:%M:%S')}
Expand All @@ -2287,7 +2287,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
error_msg = f"{type(e).__name__}: {str(e)}"
logger.exception("Job '%s' failed: %s", job_name, error_msg)

output = f"""# Cron Job: {job_name} (FAILED)
output = """# Cron Job: {job_name} (FAILED)

**Job ID:** {job_id}
**Run Time:** {_hermes_now().strftime('%Y-%m-%d %H:%M:%S')}
Expand Down
38 changes: 19 additions & 19 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ def _warn_config_parse_failure(config_path: Path, exc: Exception) -> None:

msg = (
f"Failed to parse {config_path}: {exc}. "
f"Falling back to default config — every user override "
f"(auxiliary providers, fallback chain, model settings) is being IGNORED. "
f"Fix the YAML and restart."
"Falling back to default config — every user override "
"(auxiliary providers, fallback chain, model settings) is being IGNORED. "
"Fix the YAML and restart."
)
if backup_path is not None:
msg += f" A copy of the corrupted file was saved to {backup_path}."
Expand Down Expand Up @@ -4688,20 +4688,20 @@ def warn_deprecated_cwd_env_vars(config: Optional[Dict[str, Any]] = None) -> Non
if messaging_cwd:
lines.append(
f" \033[33m⚠\033[0m MESSAGING_CWD={messaging_cwd} found in .env — "
f"this is deprecated."
"this is deprecated."
)
if terminal_cwd_env and not config_has_explicit_cwd:
# TERMINAL_CWD in env but not from config bridge — likely from .env
lines.append(
f" \033[33m⚠\033[0m TERMINAL_CWD={terminal_cwd_env} found in .env — "
f"this is deprecated."
"this is deprecated."
)
if lines:
hint_path = os.environ.get("HERMES_HOME", "~/.hermes")
lines.insert(0, "\033[33m⚠ Deprecated .env settings detected:\033[0m")
lines.append(
f" \033[2mMove to config.yaml instead: "
f"terminal:\\n cwd: /your/project/path\033[0m"
" \033[2mMove to config.yaml instead: "
"terminal:\\n cwd: /your/project/path\033[0m"
)
lines.append(
f" \033[2mThen remove the old entries from {hint_path}/.env\033[0m"
Expand Down Expand Up @@ -4911,7 +4911,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
config["stt"] = stt
save_config(config)
if not quiet:
print(f" ✓ Migrated legacy stt.model to provider-specific config")
print(" ✓ Migrated legacy stt.model to provider-specific config")

# ── Version 14 → 15: add explicit gateway interim-message gate ──
if current_ver < 15:
Expand Down Expand Up @@ -5044,7 +5044,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
if not quiet:
if grandfathered:
print(
f" ✓ Plugins now opt-in: grandfathered "
" ✓ Plugins now opt-in: grandfathered "
f"{len(grandfathered)} existing plugin(s) into plugins.enabled"
)
else:
Expand Down Expand Up @@ -6280,13 +6280,13 @@ def _check_non_ascii_credential(key: str, value: str) -> str:

print(
f"\n Warning: {key} contains non-ASCII characters that will break API requests.\n"
f" This usually happens when copy-pasting from a PDF, rich-text editor,\n"
f" or web page that substitutes lookalike Unicode glyphs for ASCII letters.\n"
f"\n"
" This usually happens when copy-pasting from a PDF, rich-text editor,\n"
" or web page that substitutes lookalike Unicode glyphs for ASCII letters.\n"
"\n"
+ "\n".join(f" {line}" for line in bad_chars[:5])
+ ("\n ... and more" if len(bad_chars) > 5 else "")
+ f"\n\n The non-ASCII characters have been stripped automatically.\n"
f" If authentication fails, re-copy the key from the provider's dashboard.\n",
+ "\n\n The non-ASCII characters have been stripped automatically.\n"
" If authentication fails, re-copy the key from the provider's dashboard.\n",
file=sys.stderr,
)
return sanitized
Expand All @@ -6306,7 +6306,7 @@ def save_env_value(key: str, value: str):
src = (managed_dir / ".env") if managed_dir else "the managed scope"
print(
f"Cannot set {key}: it is managed by your administrator ({src}) "
f"and cannot be changed.",
"and cannot be changed.",
file=sys.stderr,
)
return
Expand Down Expand Up @@ -6395,7 +6395,7 @@ def remove_env_value(key: str) -> bool:
src = (managed_dir / ".env") if managed_dir else "the managed scope"
print(
f"Cannot remove {key}: it is managed by your administrator ({src}) "
f"and cannot be changed.",
"and cannot be changed.",
file=sys.stderr,
)
return False
Expand Down Expand Up @@ -6604,7 +6604,7 @@ def show_config():
print()
print(color(
f" ⚷ Some settings are managed by your administrator ({_managed_dir}) "
f"and cannot be changed",
"and cannot be changed",
Colors.YELLOW,
Colors.BOLD,
))
Expand Down Expand Up @@ -6663,7 +6663,7 @@ def show_config():
if _env_ghost is not None and str(_env_ghost).strip() != str(_cfg_max_turns).strip():
print(color(
f" ⚠ .env has stale HERMES_MAX_ITERATIONS={_env_ghost} "
f"(run 'hermes doctor --fix' to remove)",
"(run 'hermes doctor --fix' to remove)",
Colors.YELLOW,
))
except Exception:
Expand Down Expand Up @@ -6848,7 +6848,7 @@ def set_config_value(key: str, value: str):
src = (managed_dir / "config.yaml") if managed_dir else "the managed scope"
print(
f"Cannot set '{key}': it is managed by your administrator ({src}) "
f"and cannot be changed. Contact your administrator to modify it.",
"and cannot be changed. Contact your administrator to modify it.",
file=sys.stderr,
)
sys.exit(1)
Expand Down
38 changes: 19 additions & 19 deletions hermes_cli/kanban_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,11 @@ def _rule_hallucinated_cards(task, events, runs, now, cfg) -> list[Diagnostic]:
severity="error",
title="Worker claimed cards that don't exist",
detail=(
f"The completing worker declared created_cards that either didn't "
f"exist or weren't created by its profile. The completion was "
f"blocked and the task stayed in its prior state. "
f"Usually means the worker hallucinated ids instead of capturing "
f"return values from kanban_create."
"The completing worker declared created_cards that either didn't "
"exist or weren't created by its profile. The completion was "
"blocked and the task stayed in its prior state. "
"Usually means the worker hallucinated ids instead of capturing "
"return values from kanban_create."
),
actions=actions,
first_seen_at=first,
Expand Down Expand Up @@ -462,11 +462,11 @@ def _rule_triage_aux_unavailable(task, events, runs, now, cfg) -> list[Diagnosti
severity="warning",
title=f"Triage {primary_desc} has no usable model",
detail=(
f"This task is still in triage and no working auxiliary model is "
"This task is still in triage and no working auxiliary model is "
f"visible to the dispatcher. {detail_path} The default slot uses "
f"`provider: auto` which falls back to the main model, but no main "
f"model is configured either. Configure the slot directly or set a "
f"main model so the auto fallback can take over."
"`provider: auto` which falls back to the main model, but no main "
"model is configured either. Configure the slot directly or set a "
"main model so the auto fallback can take over."
),
actions=actions,
first_seen_at=now,
Expand Down Expand Up @@ -607,16 +607,16 @@ def _rule_repeated_failures(task, events, runs, now, cfg) -> list[Diagnostic]:
f"This task has failed {failures} times in a row "
f"(most recent: {outcome_label}). Full last error:\n\n"
f"{err_snippet}\n\n"
f"The dispatcher circuit breaker is configured for "
"The dispatcher circuit breaker is configured for "
f"{failure_limit} consecutive non-success attempts. Fix the "
f"root cause and reclaim or unblock the task to retry."
"root cause and reclaim or unblock the task to retry."
)
else:
title = f"Agent {outcome_label} x{failures} (no error recorded)"
detail = (
f"This task has failed {failures} times in a row "
f"(most recent: {outcome_label}) but no error text was "
f"captured. Check the suggested command or the worker log."
"captured. Check the suggested command or the worker log."
)
return [Diagnostic(
kind="repeated_failures",
Expand Down Expand Up @@ -709,7 +709,7 @@ def _rule_repeated_crashes(task, events, runs, now, cfg) -> list[Diagnostic]:
title = f"Agent crashed {consecutive}x (no error recorded)"
detail = (
f"The last {consecutive} runs ended with outcome=crashed but "
f"no error text was captured. Check the worker log for more."
"no error text was captured. Check the worker log for more."
)
return [Diagnostic(
kind="repeated_crashes",
Expand Down Expand Up @@ -762,9 +762,9 @@ def _rule_stuck_in_blocked(task, events, runs, now, cfg) -> list[Diagnostic]:
title=f"Task has been blocked for {int(age_hours)}h",
detail=(
f"This task transitioned to blocked {int(age_hours)}h ago and "
f"has had no comments or unblock attempts since. Blocked tasks "
f"are waiting for human input — check the block reason and "
f"either unblock with feedback or answer with a comment."
"has had no comments or unblock attempts since. Blocked tasks "
"are waiting for human input — check the block reason and "
"either unblock with feedback or answer with a comment."
),
actions=actions,
first_seen_at=last_blocked_ts,
Expand Down Expand Up @@ -957,9 +957,9 @@ def _rule_stranded_in_ready(task, events, runs, now, cfg) -> list[Diagnostic]:
detail=(
f"This task has been ready for {age_str} but nothing has "
f"claimed it. Common causes: assignee {assignee!r} is "
f"misspelled, the profile was deleted, or the external "
f"worker pool for this lane is down. Confirm the assignee "
f"is correct and that a worker is actually polling for it."
"misspelled, the profile was deleted, or the external "
"worker pool for this lane is down. Confirm the assignee "
"is correct and that a worker is actually polling for it."
),
actions=actions,
first_seen_at=last_ready_ts,
Expand Down
Loading