# schedx - Technical Specification (v0.1 Build Contract)

> This document is the implementation contract for `schedx`.
>
> Historical note: parts of this spec still use the earlier working name `schedx`. Those command examples map directly to the current binary name, `schedx`.
> The goal is to hand this file to an autonomous coding agent and get a secure, reliable, useful OSS CLI.

## 0) Product Goal

`schedx` is a standalone Rust CLI that schedules commands, agent prompts, and webhooks.

Core product goals:

1. **Easy to get**: one binary, simple install (`cargo install`, release tarball, installer script)
2. **Useful quickly**: add/list/get/run/logs/history with minimal setup
3. **Reliable by design**: restarts/crashes do not permanently stop recurring schedules
4. **Secure by default**: avoid implicit shell risks, enforce safe file permissions, redact sensitive output

Non-goals for v0.1:

- Distributed scheduling
- Exactly-once delivery guarantees
- Windows backend
- Full natural-language time parsing

---

## 1) Scope

### 1.1 v0.1 MUST ship

- Actions: `run`, `prompt`, `webhook`
- Commands:
  - `add`, `list`, `get`, `run`, `rm`
  - `pause`, `resume`
  - `logs`, `history`
  - `agent add|rm|list|default`
  - `config` (read/set core keys)
  - `repair`
  - hidden: `_dispatch`, `_exec`
- Schedule formats:
  - 5-field cron
  - `in <N><m|h|d>`
  - `every <N><m|h|d>`
  - RFC3339 / ISO-8601 absolute datetime
- Backend:
  - Linux `systemd --user` (primary)
  - `none` backend for tests/dry runs

### 1.2 v0.2 (explicitly deferred)

- macOS launchd backend
- `edit` command
- natural language schedule strings (`tomorrow 9am`, `every weekday 9am`)
- manual `backup` / `restore` commands

---

## 2) Reliability Contract (Critical)

This section is normative.

### 2.1 Delivery semantics

- Scheduled execution is **at-least-once**.
- Exactly-once is not guaranteed across machine crashes.
- Recurring jobs must continue to be scheduled after reboot/restart once backend is healthy.

### 2.2 No-crash/no-restart guarantees

For recurring jobs in `active` state:

1. A `schedx` process crash must not delete or disable the schedule.
2. A machine reboot must not require re-adding jobs.
3. If `schedx` misses ticks while offline, it must catch up on next dispatch tick with policy `catch_up = latest` (run once for the latest due time, not one run per missed interval).
4. Backend drift (missing/modified systemd units) must be fixable by `schedx repair` and auto-healed on mutating commands.

### 2.3 Overlap policy

- Default concurrency policy: `forbid`
- If a job is still running when another scheduled trigger occurs, each missed due occurrence is recorded once with history status `skipped_overlap`.
- Manual `schedx run` still records a single `skipped_overlap` if the job lock is already held.
- This avoids process pileups and preserves host stability.

### 2.4 One-shot behavior

- One-shot jobs (`in ...` / ISO datetime) run once when due.
- After a successful or failed run (including timeout), status changes to `completed`.
- Completed one-shots remain in `jobs.json` for inspection/history until removed by user.

---

## 3) Security Contract

### 3.1 Command execution safety

`run` action has two modes:

1. **Default (secure)**: parse command string via `shell-words` into argv and execute directly (no shell).
2. **Shell mode**: only when `--shell` is passed; execute using `/bin/sh -lc <command>`.

This keeps default usage safe while still allowing pipes/redirection when explicitly requested.

### 3.2 Webhook safety

- Default allow only `https://` URLs.
- `http://` requires `config allow_insecure_http true`.
- Maximum webhook body size: 1 MiB.
- Request timeout enforced (job timeout or default timeout).

### 3.3 Sensitive data handling

- Data directory permissions: `0700`
- Files containing job/config/history data: `0600`
- `list`/`get` output redacts sensitive fields:
  - headers containing `authorization`, `token`, `api-key`, `cookie` (case-insensitive)
  - URL credentials (`https://user:pass@...`)
- Logs are raw command output and may contain secrets generated by user commands; this is documented.

### 3.4 Input limits

- Max command length: 32 KiB
- Max prompt length: 128 KiB
- Max number of tags per job: 20
- Max tag length: 64

Reject beyond limits with actionable errors.

---

## 4) Architecture

### 4.1 High-level model

`jobs.json` is source of truth.

`systemd` is used to trigger a **single dispatcher tick every minute**:

- `schedx _dispatch` runs on each tick
- `_dispatch` determines due jobs from `jobs.json`
- `_dispatch` launches `_exec <job-id> --scheduled-for <ts>` for due jobs
- `_exec` runs one job, writes logs/history, updates state

Why this model:

- More reliable than per-job OS timer conversion
- Simplifies catch-up logic
- Simplifies backend implementation

### 4.2 Core invariants

1. `jobs.json` drives scheduling decisions, not systemd units.
2. systemd only triggers `_dispatch`; job definitions are never stored in unit files.
3. All writes to `jobs.json`/`config.json` are atomic.
4. `_dispatch` and `_exec` must be idempotent under retries.

---

## 5) CLI Contract

### 5.1 Global behavior

- Human-readable output for TTY
- JSON output when `--json` is provided
- No ANSI colors

Global flags:

- `--json` (supported by list/get/history/config/repair)

### 5.2 Commands

#### add

```bash
schedx add <schedule> (--run <cmd> | --prompt <text> | --webhook <url>) [options]
```

Options:

- `--name <string>`
- `--tag <string>` (repeatable)
- `--timeout <seconds>`
- `--workdir <path>` (run action only)
- `--agent <name>` (prompt action only)
- `--stdin` (read `--run` command or `--prompt` text from stdin)
- `--shell` (run action only; opt-in shell execution)
- `--method <GET|POST|PUT|PATCH|DELETE>` (webhook only; default `POST`)
- `--header <Key: Value>` (repeatable, webhook only)
- `--body <string>` (webhook only)

Validation:

- Exactly one action flag required
- `--shell` only valid with `--run`
- `--stdin` only valid with `--run` or `--prompt`
- `--agent` only valid with `--prompt`

#### list

```bash
schedx list [--status active|paused|completed] [--tag <tag>] [--json]
```

#### get

```bash
schedx get <id> [--json]
```

#### run

```bash
schedx run <id>
```

Runs job immediately (`trigger = manual`) without changing schedule state.

#### rm

```bash
schedx rm <id> [--force]
```

#### pause / resume

```bash
schedx pause <id>
schedx resume <id>
```

#### logs

```bash
schedx logs <id> [--run <run-id>] [--lines <n>]
```

- Default: latest run log for job

#### history

```bash
schedx history [id] [--limit <n>] [--json]
```

#### agent

```bash
schedx agent add <name> --bin <path> [--arg <arg> ...] [--prompt-stdin]
schedx agent rm <name>
schedx agent list [--json]
schedx agent default <name>
```

#### config

```bash
schedx config
schedx config <key>
schedx config <key> <value>
```

Supported keys in v0.1:

- `default_timeout_seconds` (int)
- `backup_count` (int, >= 1)
- `log_retention_days` (int, >= 1)
- `default_agent` (string)
- `allow_insecure_http` (bool)

#### repair

```bash
schedx repair [--json]
```

Reconciles backend + state:

- Ensures dispatcher units exist and are enabled
- Validates state files
- Removes stale lock files (if any non-fcntl artifacts exist)

#### hidden commands

- `schedx _dispatch`
- `schedx _exec <job-id> --scheduled-for <rfc3339> --trigger <scheduled|manual>`

---

## 6) Schedule Parsing and Semantics

Unified parser:

```rust
fn parse_schedule(input: &str, now: DateTime<Utc>) -> Result<Schedule>
```

`Schedule`:

```rust
pub enum Schedule {
    RecurringCron { expr: String, human: String },
    OneShot { fire_at: DateTime<Utc>, human: String },
}
```

Accepted formats (in order):

1. **Cron**: exactly 5 fields, validated by `cron` crate
2. **Relative one-shot**: `in <N><m|h|d>`
3. **Relative recurring**: `every <N><m|h|d>` converted to cron
4. **ISO/RFC3339** absolute datetime

Conversion rules for `every`:

- `every Nm` -> `*/N * * * *` (N in 1..=59)
- `every Nh` -> `0 */N * * *` (N in 1..=23)
- `every Nd` -> `0 0 */N * *` (N in 1..=30)

Notes:

- Cron timezone is local system timezone at runtime.
- Invalid/unsupported schedule strings return exit code `4` with examples.

---

## 7) Data Model

### 7.1 Storage path

- If `SCHEDX_HOME` set: use it
- Else: `~/.schedx`

Directory layout:

```text
~/.schedx/
  jobs.json
  config.json
  run-history.jsonl
  backups/
    jobs-2026-02-24T10-00-00Z.json
  logs/
    <job-id>/
      <run-id>.log
  locks/
    state.lock
    dispatch.lock
    job-<id>.lock
```

### 7.2 `jobs.json`

```json
{
  "schema_version": 2,
  "jobs": {
    "a1b2c3": {
      "id": "a1b2c3",
      "name": "health-check",
      "status": "active",
      "schedule_input": "every 1h",
      "schedule": {
        "kind": "recurring_cron",
        "expr": "0 */1 * * *"
      },
      "action": {
        "kind": "run",
        "command": "python backup.py",
        "shell": false,
        "workdir": null
      },
      "timeout_seconds": 300,
      "tags": ["ops"],
      "created_at": "2026-02-24T10:00:00Z",
      "updated_at": "2026-02-24T10:00:00Z",
      "last_scheduled_at": "2026-02-24T17:00:00Z",
      "in_flight": null,
      "last_run": {
        "run_id": "r_01hzy...",
        "started_at": "2026-02-24T17:00:00Z",
        "finished_at": "2026-02-24T17:00:02Z",
        "status": "success",
        "exit_code": 0,
        "log_path": "logs/a1b2c3/r_01hzy.log"
      },
      "run_count": 42
    }
  }
}
```

`in_flight` is either `null` or:

```json
{
  "run_id": "r_01hzx...",
  "scheduled_for": "2026-02-24T18:00:00Z",
  "claimed_at": "2026-02-24T18:00:00Z"
}
```

### 7.3 `config.json`

```json
{
  "agents": {
    "opencode": {
      "bin": "/usr/local/bin/opencode",
      "args": ["run", "--format", "json", "--"],
      "prompt_stdin": false
    }
  },
  "default_agent": "opencode",
  "backup_count": 10,
  "log_retention_days": 30,
  "default_timeout_seconds": 300,
  "allow_insecure_http": false,
  "backend": "auto"
}
```

### 7.4 `run-history.jsonl`

One line per attempt:

```json
{"run_id":"r_01...","job_id":"a1b2c3","trigger":"scheduled","scheduled_for":"2026-02-24T17:00:00Z","started_at":"2026-02-24T17:00:00Z","finished_at":"2026-02-24T17:00:02Z","status":"success","exit_code":0,"log_path":"logs/a1b2c3/r_01.log"}
```

Statuses:

- `success`
- `failed`
- `timeout`
- `skipped_overlap`
- `internal_error`

---

## 8) Persistence and Crash Safety

### 8.1 Atomic write algorithm

For `jobs.json` and `config.json`:

1. Acquire exclusive state lock (`locks/state.lock` via `fs4`)
2. Read current file
3. Write new content to temp file in same directory
4. `flush` + `fsync` temp file
5. `rename` temp -> target (atomic)
6. `fsync` parent directory
7. Release lock

Before overwrite, copy previous `jobs.json` into `backups/` and rotate to `backup_count`.

### 8.2 Locking model

- Global dispatch lock (`dispatch.lock`): ensures one `_dispatch` at a time
- Per-job lock (`job-<id>.lock`): ensures no overlapping `_exec` for same job
- Locks use OS advisory locks; process death releases locks automatically

### 8.3 Retention

- On each `_dispatch`, perform lightweight cleanup:
  - remove logs older than `log_retention_days`
  - keep history file append-only (no truncation in v0.1)

---

## 9) Backend (systemd)

### 9.1 Supported backend in v0.1

- `systemd --user`
- `none` backend (testing/dry-run only)

Backend selection:

1. If `SCHEDX_BACKEND` set, honor it (`systemd` or `none`)
2. Else autodetect:
   - if Linux + `systemctl --user` works -> `systemd`
   - otherwise error with actionable message

### 9.2 Linux reboot persistence requirement

To guarantee user-level timers after reboot, lingering must be enabled.

`schedx add`/`repair` on systemd backend must check linger status.

If not enabled, fail with message:

```text
Systemd user lingering is disabled; jobs will not run after reboot until login.
Run: sudo loginctl enable-linger <your-user>
```

### 9.3 Dispatcher units

Service path:

- `~/.config/systemd/user/schedx-dispatch.service`

Timer path:

- `~/.config/systemd/user/schedx-dispatch.timer`

`schedx-dispatch.service`:

- `Type=oneshot`
- `ExecStart=<absolute-path-to-schedxx> _dispatch`

`schedx-dispatch.timer`:

- `OnCalendar=*-*-* *:*:00`
- `Persistent=true`
- `AccuracySec=1s`
- `Unit=schedx-dispatch.service`

Install/update flow:

1. write/update unit files
2. `systemctl --user daemon-reload`
3. `systemctl --user enable --now schedx-dispatch.timer`

`repair` re-runs this idempotently.

---

## 10) Execution Engine

### 10.1 `_dispatch` algorithm

Pseudocode:

```rust
fn dispatch(now: DateTime<Utc>) -> Result<()> {
    let _dispatch_lock = lock_dispatch_non_blocking()?;
    let state = load_state()?;

    for job in state.jobs.values().filter(|j| j.status == Active) {
        if let Some(scheduled_for) = compute_latest_due(job, now)? {
            spawn_exec(job.id, scheduled_for, Trigger::Scheduled)?;
        }
    }

    cleanup_old_logs()?;
    Ok(())
}
```

`compute_latest_due` rules:

- recurring:
  - find schedule occurrences `> last_scheduled_at` and `<= now`
  - if multiple due, return only latest (catch_up = latest)
- one-shot:
  - if status is `active` and `fire_at <= now` and `last_scheduled_at` is null -> due

### 10.2 `_exec` algorithm

Pseudocode:

```rust
fn exec(job_id: &str, scheduled_for: DateTime<Utc>, trigger: Trigger, run_id: Option<&str>) -> ExitCode {
    let job = load_job(job_id)?;
    if job.status != Active { return 0; }

    if trigger == Scheduled && !job.in_flight.matches(run_id, scheduled_for) {
        return 0;
    }

    let Some(_job_lock) = try_lock_job(job_id)? else {
        if trigger == Manual {
            append_history(skipped_overlap);
        }
        return 0;
    }

    let effective_run_id = run_id.unwrap_or_else(new_run_id);
    let log_path = create_log_file(job_id, effective_run_id)?;
    let result = execute_action(job, log_path)?; // success|failed|timeout|internal_error

    update_job_after_run(job_id, scheduled_for, effective_run_id, result)?;
    append_history(result)?;

    if job.is_one_shot() && !result.is_internal_error() {
        mark_completed(job_id)?;
    }

    // important: job command failure is not a schedx internal failure
    return if result.is_internal_error() { 1 } else { 0 };
}
```

Scheduled dispatch uses a persisted claim flow:

1. `_dispatch` finds the latest due occurrence.
2. It consumes `skip_remaining` immediately if applicable.
3. Otherwise it writes `in_flight = { run_id, scheduled_for, claimed_at }` to `jobs.json`.
4. Then it spawns detached `_exec --run-id <run_id>`.
5. While the claim exists, later due occurrences are recorded once each as `skipped_overlap`.
6. `_exec` clears the matching claim when it finishes.

### 10.3 Action executors

#### run action

- default: parse command via `shell_words::split` -> `Command::new(argv[0]).args(&argv[1..])`
- `--shell`: `Command::new("/bin/sh").args(["-lc", command])`
- optional working directory
- capture stdout+stderr into log file
- enforce timeout via watchdog; kill process group on timeout

#### prompt action

- lookup agent profile from config (explicit `--agent` or default)
- construct `Command::new(profile.bin).args(profile.args)`
- if `prompt_stdin`: pipe prompt to stdin
- else append prompt as final arg
- same timeout/log capture as run action

#### webhook action

- validate URL scheme (`https` unless insecure allowed)
- method default `POST`
- send with `ureq`
- include headers/body
- status 2xx => success; else `failed`

---

## 11) Output Contract

### 11.1 Human output

- concise, actionable, stable wording

Examples:

```text
Created job health-check (a1b2c3). Next run: 2026-02-24T19:00:00Z
Paused job health-check (a1b2c3)
Removed job health-check (a1b2c3)
```

### 11.2 JSON output

- deterministic keys
- UTF-8 JSON
- empty lists are `[]`

`schedx list --json` returns array of:

```json
{
  "id": "a1b2c3",
  "name": "health-check",
  "status": "active",
  "schedule_input": "every 1h",
  "next_run": "2026-02-24T19:00:00Z",
  "type": "run",
  "tags": ["ops"],
  "last_run_status": "success",
  "last_run_at": "2026-02-24T18:00:02Z"
}
```

### 11.3 Error style

- include what failed
- include quick fix when possible

Example:

```text
Error: Could not parse schedule 'every 0h'.
Accepted examples: 'in 4h', 'every 30m', '0 9 * * 1-5', '2026-03-01T14:00:00Z'
```

---

## 12) Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Internal/runtime error |
| 2 | Usage/argument error |
| 3 | Job not found |
| 4 | Schedule parse/validation error |
| 5 | Security policy violation (e.g., insecure webhook URL) |
| 6 | Backend unavailable/misconfigured |

---

## 13) Project Layout

```text
schedx/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── src/
│   ├── main.rs
│   ├── cli.rs
│   ├── commands/
│   │   ├── mod.rs
│   │   ├── add.rs
│   │   ├── list.rs
│   │   ├── get.rs
│   │   ├── run.rs
│   │   ├── rm.rs
│   │   ├── pause.rs
│   │   ├── resume.rs
│   │   ├── logs.rs
│   │   ├── history.rs
│   │   ├── agent.rs
│   │   ├── config.rs
│   │   ├── repair.rs
│   │   ├── dispatch.rs      # hidden _dispatch
│   │   └── exec.rs          # hidden _exec
│   ├── model/
│   │   ├── mod.rs
│   │   ├── job.rs
│   │   ├── action.rs
│   │   ├── schedule.rs
│   │   ├── config.rs
│   │   └── run_record.rs
│   ├── schedule/
│   │   ├── mod.rs
│   │   └── parser.rs
│   ├── engine/
│   │   ├── mod.rs
│   │   ├── dispatcher.rs
│   │   ├── executor.rs
│   │   ├── logger.rs
│   │   └── lock.rs
│   ├── backend/
│   │   ├── mod.rs
│   │   ├── systemd.rs
│   │   └── none.rs
│   ├── store/
│   │   ├── mod.rs
│   │   ├── paths.rs
│   │   ├── state.rs
│   │   ├── config.rs
│   │   ├── history.rs
│   │   ├── backup.rs
│   │   └── atomic.rs
│   ├── output/
│   │   ├── mod.rs
│   │   ├── format.rs
│   │   └── table.rs
│   └── util/
│       ├── mod.rs
│       ├── id.rs
│       └── redact.rs
├── tests/
│   ├── cli_add.rs
│   ├── cli_list.rs
│   ├── cli_lifecycle.rs
│   ├── cli_agent.rs
│   ├── cli_run.rs
│   ├── cli_history.rs
│   ├── schedule_parse.rs
│   └── helpers/
│       └── mod.rs
├── Cargo.toml
├── Cargo.lock
├── README.md
├── LICENSE
├── deny.toml
├── rustfmt.toml
├── clippy.toml
└── rust-toolchain.toml
```

---

## 14) Cargo.toml

```toml
[package]
name = "schedx"
version = "0.1.0"
edition = "2024"
rust-version = "1.85.0"
description = "Secure and reliable scheduler CLI for commands, prompts, and webhooks"
license = "MIT"
repository = "https://github.com/Alireza29675/schedxx"
homepage = "https://github.com/Alireza29675/schedxx"
readme = "README.md"
keywords = ["scheduler", "cron", "cli", "automation", "agent"]
categories = ["command-line-utilities"]

[[bin]]
name = "schedx"
path = "src/main.rs"

[dependencies]
clap = { version = "4", features = ["derive", "env"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
thiserror = "2"
chrono = { version = "0.4", features = ["serde", "clock"] }
cron = "0.15"
nanoid = "0.4"
ureq = { version = "3", default-features = false, features = ["json", "rustls"] }
fs4 = "0.12"
dirs = "6"
shell-words = "1"
url = "2"

[dev-dependencies]
assert_cmd = "2"
predicates = "3"
tempfile = "3"
insta = { version = "1", features = ["json"] }

[profile.release]
lto = true
codegen-units = 1
strip = true
panic = "abort"

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
must_use_candidate = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
```

### 14.1 Toolchain/config files

`rust-toolchain.toml`:

```toml
[toolchain]
channel = "1.85.0"
```

`rustfmt.toml`:

```toml
max_width = 100
use_field_init_shorthand = true
```

`clippy.toml`:

```toml
msrv = "1.85.0"
```

---

## 15) CI and Release

### 15.1 CI (`.github/workflows/ci.yml`)

Runs on pushes and PRs.

Jobs:

1. `check` (ubuntu)
   - `cargo fmt --check`
   - `cargo clippy --all-targets -- -D warnings`
   - install `cargo-deny` and run `cargo deny check`
2. `test` (ubuntu)
   - `cargo test --locked`
3. `docs` (ubuntu)
   - `cargo doc --no-deps`

### 15.2 Release (`.github/workflows/release.yml`)

Trigger:

- push to `main` when `Cargo.toml` version changed
- git tag `v*`

Build targets (v0.1):

- `x86_64-unknown-linux-gnu`
- `aarch64-unknown-linux-gnu`

Pipeline:

1. compute crate version from `Cargo.toml`
2. on `main`, compare against the previous `main` commit version
3. ensure tag `vMAJOR.MINOR.PATCH` exists for the release commit
4. build binaries (`cargo build --release --locked` or `cross` for aarch64)
5. package tarballs:
   - `schedx-x86_64-unknown-linux-gnu.tar.gz`
   - `schedx-aarch64-unknown-linux-gnu.tar.gz`
6. generate `checksums.txt` (sha256)
7. create GitHub release with assets + checksums
8. publish crate to crates.io

---

## 16) Installer Script (`install.sh`)

Requirements:

- POSIX shell
- `curl`
- `tar`
- `sha256sum` or `shasum -a 256`

Security requirements:

1. Download tarball and `checksums.txt`
2. Verify checksum before extraction
3. Abort on mismatch

Behavior:

- install into `${SCHEDX_INSTALL_DIR:-$HOME/.local/bin}`
- print final path and PATH hint

---

## 17) Testing Strategy

### 17.1 Test environment

- all tests must respect `SCHEDX_HOME`
- for most integration tests set `SCHEDX_BACKEND=none`

### 17.2 Required integration tests

- add/list/get/rm lifecycle
- pause/resume behavior
- manual run and log capture
- prompt action using fake agent binary (`echo` or helper script)
- webhook success/failure using local test server
- history correctness and JSON shape
- config read/write validation
- security checks:
  - `http://` webhook rejected by default
  - insecure allowed when config flag enabled
  - shell mode only when `--shell`
- overlap behavior (`skipped_overlap`)
- `repair` with missing systemd files (backend mocked or feature-gated)

### 17.3 Parser tests

- valid cron
- `in 30m`, `in 4h`, `in 2d`
- `every 30m`, `every 6h`, `every 2d`
- RFC3339 timestamp
- invalid strings rejected with parse error code

### 17.4 Reliability tests

- dispatch catch-up latest policy after simulated downtime
- one-shot runs once and transitions to completed
- atomic write survives interrupted write simulation (unit test around `atomic.rs`)

### 17.5 systemd integration tests

Optional/ignored by default, enabled with feature `test-systemd` on Linux CI job.

---

## 18) Verification Checklist (Release Gate)

Before `v0.1.0` tag, all must pass:

1. `cargo fmt --check`
2. `cargo clippy --all-targets -- -D warnings`
3. `cargo test --locked`
4. manual smoke test with real systemd backend:

```bash
export SCHEDX_HOME="$(mktemp -d)"
unset SCHEDX_BACKEND

schedx repair
schedx agent add demo --bin echo --arg AGENT:
schedx agent default demo

schedx add "every 1m" --run "echo hello" --name hello-job
schedx list

# wait >= 70 seconds
schedx history hello-job
schedx logs hello-job

schedx pause hello-job
schedx resume hello-job
schedx rm hello-job --force
```

5. installer script verifies checksums and installs binary correctly

---

## 19) Notes for the Implementing Agent

Implementation priorities:

1. Correctness and reliability over feature breadth
2. Deterministic JSON output
3. Actionable errors
4. Security defaults (no shell by default, HTTPS webhook default)

If uncertain, choose the simpler behavior that preserves the reliability/security contracts in sections 2 and 3.
