Skip to content

fix(cron): honor workdir for no_agent job scripts - #56005

Open
Frowtek wants to merge 1 commit into
NousResearch:mainfrom
Frowtek:fix/cron-no-agent-script-workdir
Open

fix(cron): honor workdir for no_agent job scripts#56005
Frowtek wants to merge 1 commit into
NousResearch:mainfrom
Frowtek:fix/cron-no-agent-script-workdir

Conversation

@Frowtek

@Frowtek Frowtek commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a bug where a no_agent cron job's configured workdir was silently ignored — the job's script always ran from HERMES_HOME/scripts/ instead of the configured working directory.

The no_agent path in run_job() called os.chdir(workdir) to set the script's working directory (its own comment: "For no_agent jobs this is just the subprocess cwd"). But _run_job_script() launches the subprocess with an explicit cwd=str(path.parent) (the scripts dir), and an explicit cwd= argument to subprocess.run() sets the child's working directory absolutely — it overrides the parent process's os.chdir(). So the chdir was a no-op for the subprocess and the workdir never took effect.

Impact: a no_agent watchdog/data-collection script that uses relative paths (./state.json, python ./sub.py, reading ./config) resolved them against the scripts directory rather than the project directory the user configured — silent wrong reads/writes or "file not found".

The fix threads the workdir into _run_job_script() as an optional cwd argument and passes it straight to subprocess.run(). It also removes the os.chdir() dance, which was process-global and unsafe while other jobs run in parallel.

Related Issue

No existing issue — found via a code audit of the cron execution path.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • cron/scheduler.py
    • _run_job_script() gains an optional cwd parameter. The subprocess now runs with cwd when it is an existing directory; otherwise it falls back to the historical default (the script's own dir under HERMES_HOME/scripts/). Backward compatible — the two callers that don't pass cwd are unchanged.
    • run_job() no_agent path: pass the job's validated workdir via _run_job_script(script_path, cwd=_job_workdir) and drop the ineffective (and process-global) os.chdir()/restore block. When the workdir no longer exists, log and fall back to running without it — mirroring the agent path's existing behavior.
  • tests/cron/test_cron_workdir.py
    • TestRunJobScriptCwdcwd honored when provided, default scripts-dir when omitted, and fallback when cwd doesn't exist.
    • TestNoAgentScriptWorkdir — end-to-end: a no_agent job's script runs in the configured workdir, defaults to the scripts dir without a workdir, and safely falls back when the workdir vanished after job creation.

How to Test

Reproduction (before the fix):

  1. Create a no_agent cron job with a workdir pointing at some project dir, and a script under HERMES_HOME/scripts/ that prints its own cwd (import os; print(os.getcwd())).
  2. Run the job. The delivered output is HERMES_HOME/scriptsnot the configured workdir. Any relative path in the script resolves against the wrong directory.

After the fix: the same job's script prints (and runs from) the configured workdir.

Automated regression tests (added in this PR):

$ pytest tests/cron/test_cron_workdir.py::TestRunJobScriptCwd \
         tests/cron/test_cron_workdir.py::TestNoAgentScriptWorkdir -v

TestRunJobScriptCwd::test_honors_cwd_when_provided                   PASSED
TestRunJobScriptCwd::test_defaults_to_scripts_dir_without_cwd        PASSED
TestRunJobScriptCwd::test_falls_back_when_cwd_missing                PASSED
TestNoAgentScriptWorkdir::test_no_agent_script_runs_in_workdir       PASSED
TestNoAgentScriptWorkdir::test_no_agent_without_workdir_uses_scripts_dir  PASSED
TestNoAgentScriptWorkdir::test_no_agent_vanished_workdir_falls_back  PASSED

6 passed

Before/after proof — with the scheduler.py fix reverted (tests kept), the end-to-end test fails, demonstrating the bug:

>   assert Path(response.strip()).resolve() == workdir.resolve()
E   AssertionError: assert <tmp>/home/scripts == <tmp>/project
FAILED ...::test_no_agent_script_runs_in_workdir

With the fix applied, all 6 pass.

Full cron suite: pytest tests/cron/571 passed (plus the 6 new). ruff check on the changed files passes clean; the Windows-footgun checker reports no findings on the diff.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(cron):)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run the affected test suite (tests/cron/) and it passes; added 6 regression tests that all pass
  • I've added tests for my changes
  • I've tested on my platform: local dev environment

Documentation & Housekeeping

  • I've updated relevant documentation (docstrings) — _run_job_script's docstring documents the new cwd argument
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact — uses subprocess.run(cwd=...) + Path.is_dir(), both cross-platform; removes a process-global os.chdir()
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

A no_agent cron job's configured `workdir` was silently ignored. The
no_agent path called os.chdir(workdir) to set the script's working
directory, but _run_job_script() runs the subprocess with an explicit
cwd=str(path.parent) (HERMES_HOME/scripts), which overrides the parent
process's cwd. The script therefore always ran from the scripts dir, so
relative paths in a watchdog/data-collection script resolved against the
wrong directory.

Thread the workdir into _run_job_script() as an optional cwd argument and
pass it straight to subprocess.run(). Drop the ineffective os.chdir dance,
which was also process-global and unsafe while parallel jobs run. When the
workdir no longer exists, log and fall back to the scripts dir (mirrors the
agent path). workdir-less jobs are unchanged.

Adds regression tests for _run_job_script(cwd=...) and the end-to-end
no_agent execution path.
@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have labels Jul 1, 2026
@Frowtek Frowtek closed this Jul 1, 2026
@Frowtek
Frowtek deleted the fix/cron-no-agent-script-workdir branch July 1, 2026 02:50
@Frowtek
Frowtek restored the fix/cron-no-agent-script-workdir branch July 1, 2026 04:33
@Frowtek Frowtek reopened this Jul 1, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating the explicit subprocess cwd override; the no-agent defect is real on current main.

Problems

  • The same helper is used by regular agent jobs, but their pre-run invocation remains _run_job_script(script_path) at cron/scheduler.py:2634. Since the helper hardcodes cwd=str(path.parent) at cron/scheduler.py:2105, configured workdir is still ignored for agent pre-run scripts.
  • Current main validates disappeared workdirs only later in the agent path. Resolve the runtime workdir before either script invocation so both modes preserve the existing fallback behavior.

Suggested changes

  • Thread one resolved/validated workdir into _run_job_script() for both the no-agent branch and the agent pre-run branch.
  • Add end-to-end coverage for both modes using a project-relative file, plus the removed-workdir fallback.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants