Compare commits

...

7 Commits

Author SHA1 Message Date
92c3eb0ab2 feat(cli): Show profile in cron list
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 1m5s
Part of #334. Displays profile name in job list when set.
2026-04-14 01:31:51 +00:00
3e7eec0b88 feat(cli): Add profile handling to cron_create and cron_edit
Part of #334. Passes profile parameter to cron API in create and edit commands.
2026-04-14 01:31:16 +00:00
3ba2907d37 feat(cli): Add --profile argument to cron create/edit
Part of #334. Adds --profile/-p argument to cron create and edit commands.
2026-04-14 01:30:54 +00:00
4b90f9a7f1 feat(cron): Add profile parameter to cronjob tool
Part of #334. Adds profile field to cronjob tool and passes it to create_job.
2026-04-14 01:29:59 +00:00
de80911ab9 feat(cron): Load profile-specific config.yaml
Part of #334. When job has profile set, loads config.yaml from profiles/PROFILE/config.yaml.
2026-04-14 01:25:49 +00:00
4dcfa11593 feat(cron): Add profile-scoped execution to scheduler
Part of #334. Loads profile-specific .env and config.yaml when job has profile set. Sets HERMES_ACTIVE_PROFILE environment variable.
2026-04-14 01:24:31 +00:00
464d0b89fb feat(cron): Add profile parameter to create_job
Part of #334. Adds profile field to job structure for profile-scoped execution.
2026-04-14 01:22:31 +00:00
5 changed files with 54 additions and 1 deletions

View File

@@ -376,6 +376,7 @@ def create_job(
provider: Optional[str] = None,
base_url: Optional[str] = None,
script: Optional[str] = None,
profile: Optional[str] = None,
) -> Dict[str, Any]:
"""
Create a new cron job.
@@ -395,6 +396,9 @@ def create_job(
script: Optional path to a Python script whose stdout is injected into the
prompt each run. The script runs before the agent turn, and its output
is prepended as context. Useful for data collection / change detection.
profile: Optional profile name for profile-scoped execution. When set, the job
runs with that profile's config.yaml and .env, and HERMES_ACTIVE_PROFILE
is set. Enables parallel execution without cross-contamination.
Returns:
The created job dict
@@ -425,6 +429,8 @@ def create_job(
normalized_base_url = normalized_base_url or None
normalized_script = str(script).strip() if isinstance(script, str) else None
normalized_script = normalized_script or None
normalized_profile = str(profile).strip() if isinstance(profile, str) else None
normalized_profile = normalized_profile or None
label_source = (prompt or (normalized_skills[0] if normalized_skills else None)) or "cron job"
job = {
@@ -455,6 +461,8 @@ def create_job(
# Delivery configuration
"deliver": deliver,
"origin": origin, # Tracks where job was created for "origin" delivery
# Profile configuration
"profile": normalized_profile, # Profile for scoped execution
}
jobs = load_jobs()

View File

@@ -682,6 +682,26 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
os.environ["HERMES_SESSION_CHAT_ID"] = str(origin["chat_id"])
if origin.get("chat_name"):
os.environ["HERMES_SESSION_CHAT_NAME"] = origin["chat_name"]
# Profile-scoped execution: load profile-specific config
profile = job.get("profile")
if profile:
os.environ["HERMES_ACTIVE_PROFILE"] = profile
profile_dir = _hermes_home / "profiles" / profile
if profile_dir.exists():
# Load profile-specific .env
profile_env = profile_dir / ".env"
if profile_env.exists():
try:
load_dotenv(str(profile_env), override=True, encoding="utf-8")
logger.info("Job '%s': Loaded profile .env from %s", job_id, profile_env)
except Exception as e:
logger.warning("Job '%s': Failed to load profile .env: %s", job_id, e)
# Profile config will be loaded later in the config section
logger.info("Job '%s': Running with profile '%s'", job_id, profile)
else:
logger.warning("Job '%s': Profile directory not found: %s", job_id, profile_dir)
# Re-read .env and config.yaml fresh every run so provider/key
# changes take effect without a gateway restart.
from dotenv import load_dotenv
@@ -700,10 +720,21 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
model = job.get("model") or os.getenv("HERMES_MODEL") or ""
# Load config.yaml for model, reasoning, prefill, toolsets, provider routing
# If profile is set, load profile-specific config
_cfg = {}
try:
import yaml
_cfg_path = str(_hermes_home / "config.yaml")
profile = job.get("profile")
if profile:
profile_cfg_path = _hermes_home / "profiles" / profile / "config.yaml"
if profile_cfg_path.exists():
_cfg_path = str(profile_cfg_path)
logger.info("Job '%s': Loading profile config from %s", job_id, _cfg_path)
else:
_cfg_path = str(_hermes_home / "config.yaml")
logger.debug("Job '%s': Profile config not found, using default: %s", job_id, _cfg_path)
else:
_cfg_path = str(_hermes_home / "config.yaml")
if os.path.exists(_cfg_path):
with open(_cfg_path) as _f:
_cfg = yaml.safe_load(_f) or {}

View File

@@ -90,6 +90,10 @@ def cron_list(show_all: bool = False):
print(f" Deliver: {deliver_str}")
if skills:
print(f" Skills: {', '.join(skills)}")
# Show profile if set
profile = job.get("profile")
if profile:
print(color(f" Profile: {profile}", Colors.MAGENTA))
script = job.get("script")
if script:
print(f" Script: {script}")

View File

@@ -4550,6 +4550,10 @@ For more help on a command:
cron_create.add_argument("--repeat", type=int, help="Optional repeat count")
cron_create.add_argument("--skill", dest="skills", action="append", help="Attach a skill. Repeat to add multiple skills.")
cron_create.add_argument("--script", help="Path to a Python script whose stdout is injected into the prompt each run")
cron_create.add_argument(
"--profile", "-p",
help="Profile name for profile-scoped execution (loads profile's config.yaml and .env)"
)
# cron edit
cron_edit = cron_subparsers.add_parser("edit", help="Edit an existing scheduled job")
@@ -4564,6 +4568,10 @@ For more help on a command:
cron_edit.add_argument("--remove-skill", dest="remove_skills", action="append", help="Remove a specific attached skill. Repeatable.")
cron_edit.add_argument("--clear-skills", action="store_true", help="Remove all attached skills from the job")
cron_edit.add_argument("--script", help="Path to a Python script whose stdout is injected into the prompt each run. Pass empty string to clear.")
cron_edit.add_argument(
"--profile", "-p",
help="Set profile for profile-scoped execution"
)
# lifecycle actions
cron_pause = cron_subparsers.add_parser("pause", help="Pause a scheduled job")

View File

@@ -233,6 +233,7 @@ def cronjob(
base_url: Optional[str] = None,
reason: Optional[str] = None,
script: Optional[str] = None,
profile: Optional[str] = None,
task_id: str = None,
) -> str:
"""Unified cron job management tool."""
@@ -270,6 +271,7 @@ def cronjob(
provider=_normalize_optional_job_value(provider),
base_url=_normalize_optional_job_value(base_url, strip_trailing_slash=True),
script=_normalize_optional_job_value(script),
profile=_normalize_optional_job_value(profile),
)
return json.dumps(
{