# Gerhard scheduled jobs

Hermes stores live scheduled jobs in:

- `agents/gerhard-hermes/cron/jobs.json`

That file is read by `hermes cron list`, `hermes cron create`, and the gateway scheduler.
It also contains mutable runtime state, such as:

- `next_run_at`
- `last_run_at`
- `last_status`
- repeat counters
- delivery errors

Because of that, `jobs.json` is not the best long-term source-of-truth for Git.
It will change merely because time passes or a job runs.

## Recommended convention

Keep desired jobs in version control here:

- `agents/gerhard-hermes/cron/desired-jobs.json`

Then materialize them into Hermes runtime jobs when bootstrapping Gerhard.

This gives us two layers:

1. Declarative schedule intent, tracked in Git.
2. Runtime scheduler state, allowed to mutate locally.

## Live Hermes commands

Inside the Gerhard container, with `HERMES_HOME=/opt/data`:

```bash
hermes cron list --all
hermes cron create "every 1d" "Your self-contained prompt here" --name "Daily reflection" --deliver local
hermes cron status
hermes cron run <job_id>
hermes cron pause <job_id>
hermes cron resume <job_id>
hermes cron remove <job_id>
```

From the host:

```bash
docker compose exec gerhard hermes cron list --all
docker compose exec gerhard hermes cron create "every 1d" "Your self-contained prompt here" --name "Daily reflection" --deliver local
```

## Schedule formats

Hermes accepts:

- `30m` — one-shot in 30 minutes
- `2h` — one-shot in 2 hours
- `every 30m` — recurring interval
- `every 2h` — recurring interval
- `0 9 * * *` — cron expression
- `2026-02-03T14:00:00` — one-shot timestamp

## Desired job format

Add entries to `desired-jobs.json` like this:

```json
{
  "version": 1,
  "jobs": [
    {
      "name": "Daily conceptual hygiene",
      "schedule": "0 8 * * *",
      "deliver": "local",
      "prompt": "Review Glitch University knowledge graph notes. Identify one concept that is vague, one relation that needs evidence, and one useful next action. Write concise output in Gerhard voice.",
      "skills": [],
      "enabled_toolsets": ["file", "terminal"],
      "workdir": "/workspace"
    }
  ]
}
```

Important:

- Prompts must be self-contained. Cron jobs run without chat context.
- Avoid secrets in prompts.
- Use `deliver: local` unless the target platform/channel is intentionally configured.
- Use absolute `workdir` values. In Gerhard's container, the mounted workspace is `/workspace`.
- Keep runtime outputs under `cron/output/`, not in Git.

## Gerhard gutask identity

The hourly orientation job expects the Gerhard container to have gutask credentials in its environment:

- `API_URL`
- `CONTENT_API_KEY`
- `AGENT_ID`
- `AGENT_NAME`
- `AGENT_PASSWORD`

The Gerhard compose service uses `env_file: .env`, so these values come from the host-local `.env` on Omega13. Do not put the password, content API key, or tokens in Git.

The Gerhard compose service also mounts the sibling checkout read-write so Gerhard can improve the tool and push changes when asked:

- `../gutasktool` -> `/opt/gutasktool`
- `./shared/knowledge` -> `/knowledge`
- `${HOME}/.ssh` -> `/root/.ssh`

Gerhard has a wrapper at `/opt/data/bin/gutask` that runs `/opt/gutasktool/gutasktool/cli.py`. Cron prompts should call the absolute wrapper path, for example:

```bash
/opt/data/bin/gutask orient --agent "$AGENT_ID"
```

When modifying gutasktool, Gerhard should work in `/opt/gutasktool`, commit normally, and push to the configured Gitea `origin` remote. Secrets stay in environment/SSH config, never in Git.

## Future improvement

Add a small bootstrap/sync script that reads `desired-jobs.json` and reconciles it into `jobs.json` by stable job name.
That lets Omega13 do:

```bash
git pull
docker compose up -d gerhard gerhard-dashboard
# optional: docker compose exec gerhard python /opt/data/cron/sync_desired_jobs.py
```

For now, `desired-jobs.json` is the version-controlled schedule manifest.
