{"id":47989792,"url":"https://github.com/tumf/agent-exec","last_synced_at":"2026-04-04T11:33:25.645Z","repository":{"id":339215481,"uuid":"1160966479","full_name":"tumf/agent-exec","owner":"tumf","description":"JSON-only stdout job runner for agent workflows (run/status/tail/wait/kill)","archived":false,"fork":false,"pushed_at":"2026-03-24T10:15:01.000Z","size":1032,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-24T12:53:55.974Z","etag":null,"topics":["agent","cli","job-runner","json","process-management","rust"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tumf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-18T15:26:57.000Z","updated_at":"2026-03-24T10:15:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tumf/agent-exec","commit_stats":null,"previous_names":["tumf/agent-exec"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/tumf/agent-exec","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tumf%2Fagent-exec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tumf%2Fagent-exec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tumf%2Fagent-exec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tumf%2Fagent-exec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tumf","download_url":"https://codeload.github.com/tumf/agent-exec/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tumf%2Fagent-exec/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31398225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","cli","job-runner","json","process-management","rust"],"created_at":"2026-04-04T11:33:25.531Z","updated_at":"2026-04-04T11:33:25.638Z","avatar_url":"https://github.com/tumf.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# agent-exec\n\nNon-interactive agent job runner. Runs commands as background jobs and returns structured JSON on stdout.\n\n## Output Contract\n\n- **stdout**: JSON by default — every command prints exactly one JSON object; pass `--yaml` to get YAML instead\n- **stderr**: Diagnostic logs (controlled by `RUST_LOG` or `-v`/`-vv` flags)\n\nThis separation lets agents parse stdout reliably without filtering log noise.\n\n## Installation\n\n```bash\ncargo install --path .\n```\n\n## Quick Start\n\n### Short-lived job (`run` → `wait` → `tail`)\n\nRun a command, wait for it to finish, then read its output:\n\n```bash\n# 1. Start the job (returns immediately with a job_id)\nJOB=$(agent-exec run echo \"hello world\" | jq -r .job_id)\n\n# 2. Wait for completion\nagent-exec wait \"$JOB\"\n\n# 3. Read output\nagent-exec tail \"$JOB\"\n```\n\nExample output of `tail`:\n\n```json\n{\n  \"schema_version\": \"0.1\",\n  \"ok\": true,\n  \"type\": \"tail\",\n  \"job_id\": \"01J...\",\n  \"stdout_tail\": \"hello world\",\n  \"stderr_tail\": \"\",\n  \"truncated\": false\n}\n```\n\n### Long-running job (`run` → `status` → `tail`)\n\nStart a background job, poll its status, then read its output:\n\n```bash\n# 1. Start the job (returns immediately with a job_id)\nJOB=$(agent-exec run sleep 30 | jq -r .job_id)\n\n# 2. Check status\nagent-exec status \"$JOB\"\n\n# 3. Stream output tail\nagent-exec tail \"$JOB\"\n\n# 4. Wait for completion\nagent-exec wait \"$JOB\"\n```\n\n### Timeout and force-kill\n\nRun a job with a timeout; SIGTERM after 5 s, SIGKILL after 2 s more:\n\n```bash\nagent-exec run \\\n  --timeout 5000 \\\n  --kill-after 2000 \\\n  sleep 60\n```\n\n## Two-step job lifecycle (create / start)\n\nIn addition to the immediate `run` path, `agent-exec` supports a two-step\nlifecycle where you define a job first and start it later.\n\n```bash\n# Step 1 — define the job (no process is spawned)\nJOB=$(agent-exec create -- echo \"deferred hello\" | jq -r .job_id)\n\n# Step 2 — launch the job when ready\nagent-exec start --wait \"$JOB\"\n```\n\n- `create` persists the command, environment, timeouts, and notification\n  settings to `meta.json` and writes `state.json` with `state=\"created\"`.\n  It returns `type=\"create\"` and the `job_id`.\n- `start` reads the persisted definition and spawns the supervisor.\n  It returns `type=\"start\"` with the same snapshot/wait payload as `run`.\n- `run` remains available as the convenience path for immediate execution.\n\n### Persisted environment\n\n`--env KEY=VALUE` values provided to `create` are stored in `meta.json` as\ndurable (non-secret) configuration and applied when `start` is called.\n`--env-file FILE` stores the file path; the file is re-read at `start` time.\n\n### State transitions\n\n| State | Meaning |\n|-------|---------|\n| `created` | Job definition persisted, no process running |\n| `running` | Supervisor and child process active |\n| `exited` | Process exited normally |\n| `killed` | Process terminated by signal |\n| `failed` | Supervisor-level failure |\n\n`kill` rejects `created` jobs (no process to signal).\n`wait` polls through `created` and `running` until a terminal state.\n`list --state created` filters to not-yet-started jobs.\n\n## Global Options\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--root \u003cPATH\u003e` | XDG default | Override the jobs root directory for all subcommands. Precedence: `--root` \u003e `AGENT_EXEC_ROOT` \u003e `$XDG_DATA_HOME/agent-exec/jobs` \u003e platform default. |\n| `--yaml` | false | Output responses as YAML instead of JSON (applies to all subcommands). |\n| `-v` / `-vv` | warn | Increase log verbosity (logs go to stderr). |\n\nThe `--root` flag is a **global** option that applies to all job-store subcommands (`run`, `status`, `tail`, `wait`, `kill`, `list`, `gc`). The preferred placement is before the subcommand name:\n\n```bash\nagent-exec --root /tmp/jobs run echo hello\nagent-exec --root /tmp/jobs status \u003cJOB_ID\u003e\nagent-exec --root /tmp/jobs list\nagent-exec --root /tmp/jobs gc --dry-run\n```\n\nFor backward compatibility, `--root` is also accepted after the subcommand name (both forms are equivalent):\n\n```bash\nagent-exec run --root /tmp/jobs echo hello\nagent-exec status --root /tmp/jobs \u003cJOB_ID\u003e\n```\n\n## Commands\n\n### `create` — define a job without starting it\n\n```bash\nagent-exec create [OPTIONS] -- \u003cCOMMAND\u003e...\n```\n\nPersists the job definition. Accepts the same definition-time options as `run`\n(command, `--cwd`, `--env`, `--env-file`, `--mask`, `--timeout`, `--kill-after`,\n`--progress-every`, `--notify-command`, `--notify-file`, `--shell-wrapper`).\nDoes **not** accept snapshot/wait options (`--snapshot-after`, `--wait`).\n\nReturns `type=\"create\"`, `state=\"created\"`, `job_id`, `stdout_log_path`,\nand `stderr_log_path`.\n\n### `start` — launch a previously created job\n\n```bash\nagent-exec start [OPTIONS] \u003cJOB_ID\u003e\n```\n\nLaunches the job whose definition was persisted by `create`. Accepts\nobservation-time options only:\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--snapshot-after \u003cms\u003e` | 10000 | Wait N ms before returning |\n| `--tail-lines \u003cN\u003e` | 50 | Lines in snapshot |\n| `--max-bytes \u003cN\u003e` | 65536 | Max bytes in snapshot |\n| `--wait` | false | Block until terminal state |\n| `--wait-poll-ms \u003cms\u003e` | 200 | Poll interval with `--wait` |\n\nReturns `type=\"start\"` with the same payload shape as `run`. Only jobs in\n`created` state can be started; any other state returns `error.code=\"invalid_state\"`.\n\n### `run` — start a background job\n\n```bash\nagent-exec run [OPTIONS] \u003cCOMMAND\u003e...\n```\n\nKey options:\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--snapshot-after \u003cms\u003e` | 10000 | Wait N ms before returning (0 = return immediately) |\n| `--timeout \u003cms\u003e` | 0 (none) | Kill job after N ms |\n| `--kill-after \u003cms\u003e` | 0 | ms after SIGTERM to send SIGKILL |\n| `--tail-lines \u003cN\u003e` | 50 | Lines of output captured in the snapshot |\n| `--cwd \u003cdir\u003e` | inherited | Working directory |\n| `--env KEY=VALUE` | — | Set environment variable (repeatable) |\n| `--mask KEY` | — | Redact secret values from JSON output (repeatable) |\n| `--tag \u003cTAG\u003e` | — | Assign a user-defined tag to the job (repeatable; duplicates deduplicated) |\n| `--wait` | false | Block until the job reaches a terminal state |\n| `--wait-poll-ms \u003cms\u003e` | 200 | Poll interval used with `--wait` |\n| `--notify-command \u003cCOMMAND\u003e` | — | Run a shell command when the job finishes; event JSON is sent on stdin |\n| `--notify-file \u003cPATH\u003e` | — | Append a `job.finished` event as NDJSON |\n| `--config \u003cPATH\u003e` | XDG default | Load shell wrapper config from a specific `config.toml` |\n| `--shell-wrapper \u003cPROG FLAGS\u003e` | platform default | Override shell wrapper for this invocation (e.g. `\"bash -lc\"`) |\n\n### `status` — get job state\n\n```bash\nagent-exec status \u003cJOB_ID\u003e\n```\n\nReturns `running`, `exited`, `killed`, or `failed`, plus `exit_code` when finished.\n\n### `tail` — read output\n\n```bash\nagent-exec tail [--tail-lines N] \u003cJOB_ID\u003e\n```\n\nReturns the last N lines of stdout and stderr.\n\n### `wait` — block until done\n\n```bash\nagent-exec wait [--timeout-ms N] [--poll-ms N] \u003cJOB_ID\u003e\n```\n\nPolls until the job finishes or the timeout elapses.\n\n### `kill` — send signal\n\n```bash\nagent-exec kill [--signal TERM|INT|KILL] \u003cJOB_ID\u003e\n```\n\n### `list` — list jobs\n\n```bash\nagent-exec list [--state created|running|exited|killed|failed] [--limit N] [--tag PATTERN]...\n```\n\nBy default only jobs from the current working directory are shown. Use `--all` to show jobs from all directories.\n\nTag filtering with `--tag` applies logical AND across all patterns. Two pattern forms are supported:\n\n- **Exact**: `--tag aaa` matches only jobs that have the tag `aaa`.\n- **Namespace prefix**: `--tag hoge.*` matches jobs with any tag in the `hoge` namespace (e.g. `hoge.sub`, `hoge.sub.deep`).\n\n```bash\n# Show jobs tagged with \"ci\"\nagent-exec list --all --tag ci\n\n# Show jobs in the \"project.build\" namespace across all directories\nagent-exec list --all --tag project.build.*\n\n# Combine: jobs tagged with both \"ci\" AND \"release\" in the current cwd\nagent-exec list --tag ci --tag release\n```\n\n### `tag set` — replace job tags\n\n```bash\nagent-exec tag set \u003cJOB_ID\u003e [--tag TAG]...\n```\n\nReplaces all tags on an existing job with the specified list. Duplicates are deduplicated preserving first-seen order. Omit all `--tag` flags to clear tags.\n\n```bash\n# Assign tags at creation time\nagent-exec run --tag project.build --tag ci -- make build\n\n# Replace tags on an existing job\nagent-exec tag set 01J9ABC123 --tag project.release --tag approved\n\n# Clear all tags\nagent-exec tag set 01J9ABC123\n```\n\n**Tag format**: dot-separated segments of alphanumeric characters and hyphens (e.g. `ci`, `project.build`, `hoge-fuga.v2`). The `.*` suffix is reserved for list filter patterns and cannot be used as a stored tag.\n\n### `notify set` — update notification configuration\n\n```bash\nagent-exec notify set \u003cJOB_ID\u003e [--command \u003cCOMMAND\u003e] \\\n  [--output-pattern \u003cPATTERN\u003e] [--output-match-type contains|regex] \\\n  [--output-stream stdout|stderr|either] \\\n  [--output-command \u003cCOMMAND\u003e] [--output-file \u003cPATH\u003e]\n```\n\nUpdates the persisted notification configuration for an existing job. This is a **metadata-only** operation: it rewrites `meta.json` and never executes sinks immediately, even when the target job is already in a terminal state.\n\n**Completion notification flags:**\n\n| Flag | Description |\n|------|-------------|\n| `--command \u003cCOMMAND\u003e` | Shell command string for the `job.finished` command sink. |\n| `--root \u003cPATH\u003e` | Override the jobs root directory. |\n\n**Output-match notification flags:**\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--output-pattern \u003cPATTERN\u003e` | — | Pattern to match against newly observed stdout/stderr lines. Required to enable output-match notifications. |\n| `--output-match-type \u003cTYPE\u003e` | `contains` | `contains` for substring matching; `regex` for Rust regex syntax. |\n| `--output-stream \u003cSTREAM\u003e` | `either` | `stdout`, `stderr`, or `either` — which stream is eligible for matching. |\n| `--output-command \u003cCOMMAND\u003e` | — | Shell command string executed on every match; event JSON is sent on stdin. |\n| `--output-file \u003cPATH\u003e` | — | File that receives one NDJSON `job.output.matched` event per match. |\n\n**Behavior**\n\n- All flags are optional; unspecified fields are preserved from the existing configuration.\n- `--command` replaces the existing `notify_command`; `notify_file` is always preserved.\n- Output-match configuration is stored under `meta.json.notification.on_output_match`.\n- Once saved, output-match settings apply only to **future** lines observed by the running supervisor — prior output is never replayed.\n- Calling `notify set` on a terminal job succeeds without executing any sink.\n- A missing job returns a JSON error with `error.code = \"job_not_found\"`.\n\n**Example — completion notification**\n\n```bash\nJOB=$(agent-exec run --snapshot-after 0 -- sleep 5 | jq -r .job_id)\nagent-exec notify set \"$JOB\" --command 'cat \u003e /tmp/event.json'\n```\n\n**Example — output-match notification**\n\n```bash\n# Run a job that may print error lines.\nJOB=$(agent-exec run --snapshot-after 0 -- sh -c 'sleep 1; echo ERROR foo' | jq -r .job_id)\n\n# Configure output-match: fire on every line containing \"ERROR\".\nagent-exec notify set \"$JOB\" \\\n  --output-pattern 'ERROR' \\\n  --output-command 'cat \u003e\u003e /tmp/matches.ndjson'\n\n# Or use a regex pattern targeting only stderr:\nagent-exec notify set \"$JOB\" \\\n  --output-pattern '^ERR' \\\n  --output-match-type regex \\\n  --output-stream stderr \\\n  --output-file /tmp/stderr_matches.ndjson\n```\n### `gc` — garbage collect old job data\n\n```bash\nagent-exec [--root \u003cPATH\u003e] gc [--older-than \u003cDURATION\u003e] [--dry-run]\n```\n\nDeletes job directories under the root whose terminal state (`exited`, `killed`, or `failed`) is older than the retention window. Running jobs are never touched.\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--older-than \u003cDURATION\u003e` | `30d` | Retention window: jobs older than this are eligible for deletion. Supports `30d`, `24h`, `60m`, `3600s`. |\n| `--dry-run` | false | Report candidates without deleting anything. |\n\n**Retention semantics**\n\n- The GC timestamp used for age evaluation is `finished_at` when present, falling back to `updated_at`.\n- Jobs where both timestamps are absent are skipped safely.\n- `running` jobs are never deleted regardless of age.\n\n**Examples**\n\n```bash\n# Preview what would be deleted (30-day default window).\nagent-exec gc --dry-run\n\n# Preview with a custom 7-day window.\nagent-exec gc --older-than 7d --dry-run\n\n# Delete jobs older than 7 days.\nagent-exec gc --older-than 7d\n\n# Operate on a specific jobs root directory.\nagent-exec --root /tmp/jobs gc --older-than 7d\n```\n\n**JSON response fields**\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `root` | string | Resolved jobs root path |\n| `dry_run` | bool | Whether this was a preview-only run |\n| `older_than` | string | Effective retention window (e.g. `\"30d\"`) |\n| `older_than_source` | string | `\"default\"` or `\"flag\"` |\n| `deleted` | number | Count of directories actually deleted |\n| `skipped` | number | Count of directories skipped |\n| `freed_bytes` | number | Bytes freed (or would be freed in dry-run) |\n| `jobs` | array | Per-job details: `job_id`, `state`, `action`, `reason`, `bytes` |\n\nThe `action` field in each `jobs` entry is one of:\n- `\"deleted\"` — directory was removed\n- `\"would_delete\"` — would be removed in a real run (dry-run only)\n- `\"skipped\"` — preserved with an explanation in `reason`\n\n## delete\n\nExplicitly remove one or all finished job directories. Unlike `gc`, which uses\nage-based retention across the whole jobs root, `delete` is operator-driven:\nremove one known job immediately, or clear finished jobs belonging to the\ncurrent working directory.\n\n```\nagent-exec delete \u003cJOB_ID\u003e\nagent-exec delete --all [--dry-run]\n```\n\n**State rules**\n\n- `delete \u003cJOB_ID\u003e` — removes jobs in state `created`, `exited`, `killed`, or\n  `failed`. Returns an error for `running` jobs (the job directory is preserved).\n- `delete --all` — removes only terminal jobs (`exited`, `killed`, `failed`)\n  whose persisted `meta.json.cwd` matches the caller's current working\n  directory. Jobs in `created` or `running` state are skipped and reported in\n  the response.\n\n**Examples**\n\n```bash\n# Remove a specific finished job.\nagent-exec delete 01JA1B2C3D4E5F6G7H8I9J0K1L\n\n# Preview which jobs would be removed from the current directory.\nagent-exec delete --dry-run --all\n\n# Remove all terminal jobs from the current directory.\nagent-exec delete --all\n\n# Operate on a specific jobs root.\nagent-exec --root /tmp/jobs delete --all\n```\n\n**JSON response fields**\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `root` | string | Resolved jobs root path |\n| `dry_run` | bool | Whether this was a preview-only run |\n| `deleted` | number | Count of directories actually deleted (0 when `dry_run=true`) |\n| `skipped` | number | Count of in-scope directories that were not deleted |\n| `jobs` | array | Per-job details: `job_id`, `state`, `action`, `reason` |\n\nThe `action` field in each `jobs` entry is one of:\n- `\"deleted\"` — directory was removed\n- `\"would_delete\"` — would be removed in a real run (dry-run only)\n- `\"skipped\"` — preserved with an explanation in `reason` (e.g. `\"running\"`, `\"created\"`)\n\n**Difference between `delete` and `gc`**\n\n| | `delete` | `gc` |\n|--|----------|------|\n| Scope | Single job or cwd-scoped finished jobs | Entire jobs root |\n| Trigger | Explicit operator action | Age-based retention policy |\n| Running jobs | Always rejected / skipped | Always skipped |\n| Dry-run | `--dry-run` flag | `--dry-run` flag |\n\n## serve — HTTP API server\n\n`agent-exec serve` starts a REST API server that exposes job operations over HTTP.\nThis allows Flowise, curl, or any HTTP client to launch and monitor jobs without\nneeding direct access to the CLI.\n\n```bash\nagent-exec serve [--bind HOST:PORT] [--port PORT]\n```\n\n**Default bind address**: `127.0.0.1:19263` (localhost only, not exposed externally).\n\n### Network security note\n\nThe server performs **no authentication**. Access is controlled by the bind address:\n- `127.0.0.1` (default): only reachable from the same host — safe for local use.\n- `0.0.0.0`: reachable from all network interfaces — **requires a firewall or reverse proxy** to restrict access.\n\n### Endpoints\n\n| Method | Path            | CLI equivalent | Description                                      |\n|--------|-----------------|----------------|--------------------------------------------------|\n| GET    | /health         | —              | Health check. Returns `{\"ok\":true}`              |\n| POST   | /exec           | `run`          | Launch a job; returns `job_id`                   |\n| GET    | /status/{id}    | `status`       | Job status                                       |\n| GET    | /tail/{id}      | `tail`         | stdout/stderr log tail                           |\n| GET    | /wait/{id}      | `wait`         | Block until job reaches a terminal state         |\n| POST   | /kill/{id}      | `kill`         | Send SIGTERM to the job                          |\n\nAll responses include `schema_version`, `ok`, and `type` fields matching the CLI schema.\n\n### POST /exec request body\n\n```json\n{\n  \"command\": [\"bash\", \"-c\", \"echo hello\"],\n  \"cwd\": \"/tmp\",\n  \"env\": {\"FOO\": \"bar\"},\n  \"timeout_ms\": 30000,\n  \"wait\": false\n}\n```\n\nOnly `command` is required. Returns the same `RunData` as the `run` CLI command.\n\n### Flowise / Docker example\n\nFrom a Flowise container, use `host.docker.internal` to reach the agent-exec server\nrunning on the host:\n\n```\nPOST http://host.docker.internal:19263/exec\n{\"command\": [\"my-agent-script\"]}\n```\n\nThen poll `GET http://host.docker.internal:19263/wait/{job_id}` until the job finishes.\n\nTo allow container access, start the server with `--bind 0.0.0.0:19263` and ensure\nyour firewall does **not** expose port 19263 to the public internet.\n\n## Configuration\n\n`agent-exec` reads an optional `config.toml` to configure the shell wrapper used for command-string execution.\n\n### Config file location\n\n- `$XDG_CONFIG_HOME/agent-exec/config.toml` (defaults to `~/.config/agent-exec/config.toml`)\n\n### `config.toml` format\n\n```toml\n[shell]\nunix    = [\"sh\", \"-lc\"]   # used on Unix-like platforms\nwindows = [\"cmd\", \"/C\"]   # used on Windows\n```\n\nBoth keys are optional. Absent values fall back to the built-in platform default (`sh -lc` / `cmd /C`).\n\n### Shell wrapper precedence\n\n1. `--shell-wrapper \u003cPROG FLAGS\u003e` CLI flag (highest priority)\n2. `--config \u003cPATH\u003e` explicit config file\n3. Default XDG config file (`~/.config/agent-exec/config.toml`)\n4. Built-in platform default (lowest priority)\n\n### Command launch modes (Unix)\n\n`agent-exec run` supports two launch modes, selected by the number of arguments after `--`:\n\n| Mode | Example | Behaviour |\n|------|---------|-----------|\n| **Shell-string** | `agent-exec run -- \"echo hi \u0026\u0026 ls\"` | Single argument is passed as-is to the shell wrapper. Shell operators (`\u0026\u0026`, pipes, etc.) are preserved. The wrapper process is the workload boundary. |\n| **Argv** | `agent-exec run -- cflx run` | Two or more arguments trigger an `exec \"$@\"` handoff. The shell wrapper runs briefly for login-shell environment initialisation, then replaces itself with the target workload. The observed child PID and lifecycle align with the intended command, not the shell. |\n\nThe `exec` handoff means that for argv-mode invocations, completion tracking aligns with the target workload rather than the shell wrapper, which resolves lingering-shell issues when the target replaces the wrapper process.\n\nThe configured wrapper applies to **both** `run` command-string execution and `--notify-command` delivery. Notify delivery always uses shell-string mode regardless of how the job was launched.\n\n### Override per invocation\n\n```bash\nagent-exec run --shell-wrapper \"bash -lc\" -- my_script.sh\n```\n\n### Use a custom config file\n\n```bash\nagent-exec run --config /path/to/config.toml -- my_script.sh\n```\n\n## Job Finished Events\n\nWhen `run` is called with `--notify-command` or `--notify-file`, `agent-exec` emits a `job.finished` event after the job reaches a terminal state.\n\n- `--notify-command` accepts a shell command string, executes it via the configured shell wrapper (default: `sh -lc` on Unix, `cmd /C` on Windows), and writes the event JSON to stdin.\n- `--notify-file` appends the event as a single NDJSON line.\n- `completion_event.json` is also written in the job directory with the event plus sink delivery results.\n- Notification delivery is best effort; sink failures do not change the main job state.\n- When delivery success matters, inspect `completion_event.json.delivery_results`.\n\nChoose the sink based on the next consumer:\n\n- Use `--notify-command` for small, direct reactions such as forwarding the event back to the launching OpenClaw session with `openclaw agent --deliver --reply-channel ... --session-id ... -m ...`.\n- Use `--notify-file` when you want a durable queue-like handoff to a separate worker that can retry or fan out.\n- Prefer a compact one-liner for agent-authored OpenClaw callbacks, and prefer `AGENT_EXEC_EVENT_PATH` over parsing stdin when the downstream command accepts a file.\n\nExample:\n\n```bash\nagent-exec run \\\n  --wait \\\n  --notify-file /tmp/agent-exec-events.ndjson \\\n  -- echo hello\n```\n\nCommand sink example:\n\n```bash\nagent-exec run \\\n  --wait \\\n  --notify-command 'cat \u003e /tmp/agent-exec-event.json' \\\n  -- echo hello\n```\n\n### OpenClaw examples\n\n#### Return the event to the launching OpenClaw session\n\nThis pattern is often more flexible than sending a final user message directly from the notify command. The launching session can inspect logs, decide whether the result is meaningful, and summarize it in context. In same-host agent-to-agent flows, `job_id` plus `event_path` is a good default.\n\nCall `openclaw agent --deliver` with the reply channel and session id directly:\n\n```bash\nSESSION_ID=\"01bb09d5-6485-4a50-8d3b-3f6e80c61f9c\"\nREPLY_CHANNEL=\"telegram\"\n\nagent-exec run \\\n  --notify-command \"openclaw agent --deliver --reply-channel $REPLY_CHANNEL --session-id $SESSION_ID -m \\\"job_id=\\$AGENT_EXEC_JOB_ID event_path=\\$AGENT_EXEC_EVENT_PATH\\\"\" \\\n  -- ./scripts/run-heavy-task.sh\n```\n\nWith this pattern, the receiving OpenClaw session can open the persisted event file immediately and still keep the job id for follow-up commands.\n\nPrefer sending `job_id` and `event_path` instead of the full JSON blob when the receiver can access the same filesystem.\n\n#### Attach or replace the callback later with `notify set`\n\nUse `notify set` when the job is already running and you only learn the OpenClaw destination afterward.\n\n```bash\nJOB=$(agent-exec run --snapshot-after 0 -- ./scripts/run-heavy-task.sh | jq -r .job_id)\nSESSION_ID=\"01bb09d5-6485-4a50-8d3b-3f6e80c61f9c\"\nREPLY_CHANNEL=\"telegram\"\n\nagent-exec notify set \"$JOB\" \\\n  --command \"openclaw agent --deliver --reply-channel $REPLY_CHANNEL --session-id $SESSION_ID -m \\\"job_id=\\$AGENT_EXEC_JOB_ID event_path=\\$AGENT_EXEC_EVENT_PATH\\\"\"\n```\n\n`notify set` is metadata-only: it updates the stored callback for future completion delivery and does not execute the sink immediately.\n\n#### Durable file-based worker\n\nUse `--notify-file` when you want retries or fanout outside the main job lifecycle:\n\n```bash\nagent-exec run \\\n  --notify-file /var/lib/agent-exec/events.ndjson \\\n  -- ./scripts/run-heavy-task.sh\n```\n\nA separate worker can tail or batch-process the NDJSON file, retry failed downstream sends, and route events to chat, webhooks, or OpenClaw sessions without coupling that logic to the main job completion path.\n\n### Operational guidance\n\n- `--notify-command` accepts a plain shell command string; no JSON encoding is needed.\n- Keep notify commands small, fast, and idempotent.\n- Prefer `AGENT_EXEC_EVENT_PATH` when the downstream command already knows how to read a file.\n- Common sink failures include quoting mistakes, PATH or env mismatches, downstream non-zero exits, and wrong chat, session, or delivery-mode targets.\n- If you need heavier orchestration, let the notify sink hand off to a checked-in helper or durable worker.\n\nFor command sinks, the event JSON is written to stdin and these environment variables are set:\n\n- `AGENT_EXEC_EVENT_PATH`: path to the persisted event file (`completion_event.json` for `job.finished`, `notification_events.ndjson` for `job.output.matched`)\n- `AGENT_EXEC_JOB_ID`: job id\n- `AGENT_EXEC_EVENT_TYPE`: `job.finished` or `job.output.matched`\n\nExample `job.finished` payload:\n\n```json\n{\n  \"schema_version\": \"0.1\",\n  \"event_type\": \"job.finished\",\n  \"job_id\": \"01J...\",\n  \"state\": \"exited\",\n  \"command\": [\"echo\", \"hello\"],\n  \"cwd\": \"/path/to/cwd\",\n  \"started_at\": \"2026-03-15T12:00:00Z\",\n  \"finished_at\": \"2026-03-15T12:00:00Z\",\n  \"duration_ms\": 12,\n  \"exit_code\": 0,\n  \"stdout_log_path\": \"/jobs/01J.../stdout.log\",\n  \"stderr_log_path\": \"/jobs/01J.../stderr.log\"\n}\n```\n\nIf the job is killed by a signal, `state` becomes `killed`, `exit_code` may be absent, and `signal` is populated when available.\n\n## Output-Match Events\n\nWhen a job has output-match notification configuration (set via `notify set --output-pattern`), the running supervisor evaluates each newly observed stdout/stderr line and emits a `job.output.matched` event for every line that matches.\n\n**Key properties:**\n\n- Delivery fires on **every matching line**, not once per job.\n- Only **future** lines are eligible — output produced before `notify set` was called is never replayed.\n- Sink failures are recorded in `notification_events.ndjson` and do not affect the job lifecycle state.\n- Matching uses either `contains` (substring) or `regex` (Rust regex syntax) as configured by `--output-match-type`.\n- Stream selection (`--output-stream`) restricts matching to `stdout`, `stderr`, or `either`.\n\nExample `job.output.matched` payload:\n\n```json\n{\n  \"schema_version\": \"0.1\",\n  \"event_type\": \"job.output.matched\",\n  \"job_id\": \"01J...\",\n  \"pattern\": \"ERROR\",\n  \"match_type\": \"contains\",\n  \"stream\": \"stdout\",\n  \"line\": \"ERROR: connection refused\",\n  \"stdout_log_path\": \"/jobs/01J.../stdout.log\",\n  \"stderr_log_path\": \"/jobs/01J.../stderr.log\"\n}\n```\n\nDelivery records for output-match events are appended to `notification_events.ndjson` in the job directory (one JSON object per line). The `completion_event.json` file retains only `job.finished` delivery results.\n\n## Logging\n\nLogs go to **stderr** only. Use `-v` / `-vv` or `RUST_LOG`:\n\n```bash\nRUST_LOG=debug agent-exec run echo hello\nagent-exec -v run echo hello\n```\n\n## Development\n\n```bash\ncargo build\ncargo test --all\ncargo fmt --all\ncargo clippy --all-targets --all-features -- -D warnings\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftumf%2Fagent-exec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftumf%2Fagent-exec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftumf%2Fagent-exec/lists"}