# Jobs and runners

A job is an ordinary executable that Corotation runs after relevant changes reach `current`. A runner is a machine that executes those jobs. Use the same commands whether the runner lives on your laptop, beside `current`, or on another host.

## Start with a script

Create `scripts/release.sh`. Write outputs to `$LAPLACE_OUTPUT`, and keep expensive compiler data in `$LAPLACE_CACHE`:

```sh
#!/bin/sh
set -eu
cargo build --release --locked
cp "$CARGO_TARGET_DIR/my-game" "$LAPLACE_OUTPUT/my-game"
```

Then, inside your connected project:

```sh
cor jobs add release
cor runner
cor jobs
```

`jobs add` suggests your script, asks which platforms should run it, saves `laplace.jobs.toml`, and explicitly enables the definition on `current`. Keep `cor work` running to synchronize the script and source. Setup doesn't publish unfinished local edits by itself.

`runner` remembers the remote, generates a separate SSH identity, detects the platform, and offers trusted native execution or a Docker image. On macOS it installs a LaunchAgent; on Linux it installs a systemd user service. Both start at login. A Linux host without a user service manager can use `cor runner --foreground` under its existing supervisor. Nothing runs on a machine until you start a runner there.

After setup, `cor runner` needs only its own saved identity, even when your personal SSH agent is locked. `cor runner stop` works offline and disables startup. `cor runner settings` shows the saved configuration. Each runner has one execution slot. Add machines to increase capacity.

## Run on multiple machines

Select `linux-x86_64,macos-arm64` during job setup. On each machine, connect a folder to the same repository, then run:

```sh
cor runner
```

Each run contains one task per target. All targets use **one immutable source snapshot**. Linux cannot build today's source while macOS accidentally builds tomorrow's. A run's artifacts become downloadable after every target succeeds.

Runner connections are outbound SSH, using the existing Corotation address and port. No incoming port or personal key is needed by the background worker. Native runners detect the host platform; Docker runners detect the Docker daemon's platform. `--target LABEL` supports explicitly configured cross-compilers. Labels express capability; Corotation does not install a cross-compiler.

## Configuration

```toml
[jobs.release]
on = "published"
run = ["sh", "scripts/release.sh"]
inputs = ["src/**", "assets/**", "Cargo.*", "scripts/**", "!assets/previews/**"]
quiet = "30s"
max_wait = "2m"
timeout = "30m"
targets = ["linux-x86_64", "macos-arm64"]
```

`run` is an argument array, not a shell expression. Use `sh` explicitly for shell scripts. `inputs` uses glob patterns; any matching exclusion wins. Defaults are all paths, 30 seconds of quiet, a two-minute maximum batching window, a 30-minute command timeout, and the platform where the job is added. Durations accept `ms`, `s`, `m`, or `h`, up to 24 hours. `on = "manual"` disables automatic triggers.

After editing a definition, deliberately apply it:

```sh
cor jobs enable release
```

Synchronizing a configuration file never enables commands or changes an enabled definition. Repository members can enable jobs; runner keys cannot. Changes to an enabled job affect future runs; existing runs and retries keep the definition they started with. Script contents come from each run's source snapshot.

## Inspect, retry, or stop

```sh
cor jobs
cor jobs logs release --follow
cor jobs run release
cor jobs retry 42
cor jobs artifacts 42
cor jobs disable release
cor jobs runners
```

The status list shows the latest 50 runs, their targets and runner names, and any pending changes. In a terminal, enter a run number to inspect its logs. `jobs logs --run 42` and `jobs artifacts 42` can retrieve older runs by ID. `--json` disables prompts and prints structured results.

Logs retain the latest 128 KiB per task and refresh with the ten-second heartbeat. Put complete logs in `$LAPLACE_OUTPUT` if you need to archive them. Artifacts default to a new temporary folder; use `--output` for a new folder outside the synchronized project, or inside `.laplace`. A task supports up to 1,000 files / 10 GiB. Symlinks are rejected.

`jobs disable` cancels queued work and invalidates active leases. Running commands stop at their next heartbeat. To revoke a specific runner, use `cor jobs runners --revoke FINGERPRINT`. Removing the personal key that enrolled a runner also revokes that runner.

## Script contract

| Value | Meaning |
| --- | --- |
| Working directory | Exact source tree selected for this run |
| `LAPLACE_CONTEXT` | Path to versioned JSON: repository, sequence, job, run, task, attempt, target, and triggering changes with SSH authors and session tags |
| `LAPLACE_TASK_ID` | Stable task ID across automatic and manual retries; use for idempotent side effects |
| `LAPLACE_RUN_ID` | Run number within this repository |
| `LAPLACE_SEQUENCE` | Source sequence shared by every target |
| `LAPLACE_CACHE` | Persistent local cache per job/target on this runner |
| `CARGO_TARGET_DIR` | `$LAPLACE_CACHE/target`, configured automatically |
| `LAPLACE_STATE` | Persistent local job state; **not shared between runners** |
| `LAPLACE_OUTPUT` | Fresh directory for artifacts from this attempt |

Exit zero for success, nonzero for failure. Standard output and standard error become the task's log. Shells, Python, compiled programs, or an AI CLI can all use this contract. Corotation does not impose a release policy or invoke an AI service itself.

A release-decision script can read the triggering changes, compare with its last released version, and exit successfully without outputs when a release is unnecessary. A decision that must survive moving to another runner belongs in durable external storage, keyed by repository and task ID; runner-local state is only a convenience.

## Performance and recovery

Publication records matching packages once in the same SQLite transaction as the accepted package. It never starts a subprocess or copies the project. Runners drive scheduling after the quiet window; status requests also advance scheduling. If no runner or status client is connected, pending changes stay durable until one returns.

There is one active run per job and one coalesced pending batch. Changes during a build become the next run. A runner downloads only missing source chunks, preserves unchanged source timestamps, and reuses its local compiler cache. Untracked build products in the source directory are removed before the next run; put reusable data in the cache. Incoming synchronization never edits a build workspace.

Task leases last 60 seconds and renew every ten seconds. A disconnected runner stops work when it cannot renew; expired tasks can be reassigned, up to three attempts before failing. New attempt tokens prevent stale workers from replacing a result. Explicit retry runs failed targets against the original snapshot. Shell side effects are **at least once**: use the task ID as an idempotency key when publishing externally.

Run manifests, task results, logs, and artifact chunks survive server restarts and baseline compaction. Artifacts are separate from source chunks and never trigger new jobs. There is no automatic history/artifact/cache garbage collection yet; include the server's jobs directory and SQLite database in backups and monitor disk usage.

## Execution and trust

For trusted repository code, first setup can use `cor runner --native`. Native scripts execute as your OS account and retain its home directory, so installed toolchains and CLI authentication keep working. A cleaned environment and separate working directory are not a security sandbox; scripts can still access that account's files and network.

For isolation, pull a prepared image, then use `cor runner --image IMAGE`. Corotation pins its local image ID and runs it without network, host credentials, extra capabilities, or a writable container root. It mounts only source, job cache/state, outputs, and read-only context. Dependencies must already be in the image or prepared cache. CPU, memory, process count, and temporary storage are bounded. Docker must be installed and running on that machine.

`cor runner --jobs release` limits an enrolled runner to named jobs. By default it accepts all explicitly enabled jobs in this repository. It can read repository chunks and exchange its own task results, but cannot publish source, manage jobs, or access team/session APIs. Runner keys and settings are stored privately outside the project under `~/.local/share/laplace/runners/`.
