> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nika.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Arming a project

> Declare beats in nika.yaml and let the machine fire them — the read-only report, the one firer, the OS bridge, and the resident firer.

<Info>
  **The file proposes, the machine disposes.** Arming a project means
  declaring *beats* in `nika.yaml` — what runs, when, under which
  policies — and letting the machinery fire them: an OS unit
  (`launchd` · `systemd`), or the resident `nika serve` where no OS
  scheduler exists. Reading this page schedules nothing; firing is
  always a machine act.
</Info>

## The `arm:` block

Each entry under `arm:` in `nika.yaml` is one beat:

```yaml theme={"system"}
nika: v1
ceiling: 0.50            # the run default when a beat says nothing — a beat always says its own

arm:
  - workflow: workflows/nightly-report.nika.yaml
    cadence: "TZ=Europe/Paris 30 7 * * *"
    plafond: 0.20
    manqué: rattraper-une-fois
    chevauchement: sauter
    après_saut: prochain-créneau
  - workflow: workflows/on-event.nika.yaml
    cadence: on-webhook
    plafond: 0.05
    manqué: sauter
```

| Key                             | Required | What it means                                                                                                                                                                                     |
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workflow`                      | yes      | The workflow to fire — a repo-relative `*.nika.yaml` path                                                                                                                                         |
| `cadence`                       | yes      | `on-webhook`, a 5-field cron (`"TZ=Europe/Paris 30 7 * * *"`), or a readable schedule such as `"TZ=Europe/Paris lundi 9h07"` — the IANA zone resolves against the embedded tzdb, never the host's |
| `plafond`                       | yes      | Per-tick cost ceiling in USD — no default exists (the pay law: a beat always names its own)                                                                                                       |
| `manqué`                        | yes      | The missed-run policy — no default either (the run-missed law)                                                                                                                                    |
| `chevauchement`                 | no       | Overlap — `sauter` (default) and `file` are live today. `remplacer` is accepted by the registry but the firer refuses it until serve v0.2 rather than approximating it                            |
| `après_saut`                    | no       | After an overlap skip — `prochain-créneau` is the live default. `à-complétion` is accepted by the registry but refused by the current firer until serve v0.2                                      |
| `où`                            | no       | `local` (default) · `cloud` — the calendar stays in the registry; a `cloud` beat skips honestly at fire today (the paid cloud lane is ③)                                                          |
| `actif` + `raison` + `jusqu_au` | together | The suspension triple — `actif: false` requires the reason and the expiry date. **Deleting the line does not disarm** (law N4); the triple does                                                   |
| `tolérance`                     | no       | Declares an (m,k)-firm skip tolerance (`3/4`). The current engine parses and reports it but does not enforce the outage threshold yet                                                             |
| `décalage`                      | no       | Reserved deterministic jitter (`hash` only). The current firer refuses it until serve v0.2; slots fire at their declared instant today                                                            |
| `par`                           | no       | Declares the human who armed it — proves nothing by itself (N3: the machine's key is what authorizes; a merge arms nothing)                                                                       |

`signature:` and `budget:` are reserved and **refused** today — they
fail loudly during registry validation rather than silently doing nothing.

### The miss policies (`manqué:`)

| Value                | What a silence does                                                                                        |
| -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `rattraper`          | Reserved for serve v0.2. The current firer refuses it rather than silently collapsing several missed slots |
| `rattraper-une-fois` | Fire ONE catch-up for the whole silence                                                                    |
| `sauter`             | Never catch up — a skip is an event, never an execution                                                    |

## `nika arm` — the report

Bare, the verb READS the registry and reports what is armed, what
fired, and what fires next — it schedules nothing:

```text theme={"system"}
$ nika arm
2 beats in /home/alice/project/nika.yaml

  [armed] workflows/nightly-report.nika.yaml · TZ=Europe/Paris 30 7 * * *
         ✓ PROUVÉ · fired · 2026-08-19T05:30:01Z · slot 2026-08-19T05:30:00Z · 0 sauts / 12 tirs
         → 2026-08-20 07:30 CEST
         → 2026-08-21 07:30 CEST
         → 2026-08-22 07:30 CEST

  [armed] workflows/on-event.nika.yaml · on-webhook
         · DÉCLARÉ — le registre le dit, la machine ne l'a jamais tiré
         → no upcoming slot (a webhook beat fires on its event)

nothing was scheduled — `nika arm` READS the file. The machine that fires them is the arming edge.
```

The proof line separates **PROUVÉ** from **DÉCLARÉ** (law N3): the
registry *declares*, only the sidecar *proves* the machine fired. A beat
carrying the suspension triple is reported `idle` and never computed —
asking a disarmed beat for its next slot would print a date nobody will
see fire. A `par:` line is shown for what it is: declared, not verified.

Sidecar directories no registry entry names are the **orphelins** —
reported, NEVER erased (law N4). Exit `0` clean · exit `2` when the
registry refuses, and the refusal teaches the fix.

`on-webhook` is a declaration seam today, not a live listener. The report
shows no clock slot and `arm fire` records an honest webhook skip; an event
bridge that actually delivers the beat has not shipped yet.

## `nika arm fire <label>` — the one firer

```text theme={"system"}
$ nika arm fire nightly-report
fired nightly-report · slot 2026-08-19T05:30:00Z · exit 0
```

ONE function applies the on-time window, the miss policy, the overlap
lock, the per-tick ceiling and the firing record — the emitted OS units
and `nika serve` both end here, so the law lives exactly once. The label
is the workflow file's radical (`workflows/doctor.nika.yaml` → `doctor`;
a radical collision takes `-2`, `-3` in file order).

The stdout contract: a decision prints **exactly one line** —
`fired …` · `skipped …` · `paused …` · `failed …` — so a launchd or
serve log stays byte-pure. Two honest exceptions: a registry refusal
(exit `2`) teaches multi-line, and an environment death (exit `3`)
rides stderr.

What a fire guarantees:

* The per-beat lock is taken BEFORE the decision and held until AFTER
  the receipt lands — **at-least-once**: a crash between the claim and
  its receipt leaves a durable unmatched claim in `history.ndjson`, never
  a silent double-fire. The current report does not fold that unsettled
  claim into its own line yet. Exactly-once is never promised; when the
  workflow's effects are idempotent, the result is effectively-once.
* Every fire is a FRESH run (law N2): a run you pause is parked with its
  trace (`paused … · trace …`), never resumed by the firer.
* The clock is injectable (hidden `--now`) so a replay and a test are
  deterministic.

| Exit | Meaning                                                                            |
| ---- | ---------------------------------------------------------------------------------- |
| `0`  | Clean — fired, or honestly skipped by policy                                       |
| `1`  | The run itself failed (the workflow's exit)                                        |
| `2`  | A project, registry, or target workflow file refused — the message teaches the fix |
| `3`  | Environment failure                                                                |
| `4`  | The run was paused and parked with its trace                                       |

What a fire leaves — the sidecar `.nika/arm/<label>/`:

| File             | Role                                                                                                                                                                         |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `history.ndjson` | The versioned ledger (`nika/arm-event@1`), hash-chained and verified before every append. A journaled skip is one event; an attempted run is a claim followed by its receipt |
| `last.json`      | The projection of the last slot-bearing decision (fired, skipped, paused, or failed) — what the report reads                                                                 |
| `watermark`      | The timestamp of the latest recorded decision                                                                                                                                |
| `ledger.lock`    | A transient inner lock that serializes verify + append; normal RAII cleanup removes it after the operation                                                                   |

## `nika arm disarm <label>`

Prints the disarm gesture (law N4): removing the line from the file is
not it — `actif: false` + `raison:` + `jusqu_au:` is, because that
leaves an auditable trail. `--write` removes only generated unit files
and prints the `launchctl bootout` or `systemctl --user disable --now`
command for the operator; it never executes that unload command itself.
When it removed at least one unit, it then attempts to journal the gesture
and reports a journal failure even though the file removal already happened.

## `nika arm --emit` — the OS bridge

`--emit launchd|systemd` prints the OS unit that fires the beats
instead of reading the registry; add `--write` to install it:

| Flag                  | With `--emit`                                                                                                                        |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `--write`             | Write the unit file instead of printing it                                                                                           |
| `--out <dir>`         | The directory the unit writes to                                                                                                     |
| `--mode user\|system` | `user` is the live scope (and default). `system` is present in the CLI vocabulary but refused until the root-scoped serve lane ships |
| `--env-file <file>`   | The env file the unit loads — provider keys live there, NEVER in the unit                                                            |
| `--nika-bin <path>`   | The nika binary the unit invokes                                                                                                     |

`launchd` emits a macOS user agent
(`~/Library/LaunchAgents/nika.arm.<radical>.plist`); `systemd` emits a
user timer + service pair. Any of these flags set WITHOUT `--emit`
refuses honestly — a flag that does nothing is a lie.

## `nika serve` — the resident firer

In normal mode, the resident loop ends at the same firer as an emitted OS
unit — for machines where no OS scheduler exists (a container, a bare
server, a VM you own):

```text theme={"system"}
$ nika serve --once     # one sweep: fire every due beat, then exit
$ nika serve --dry      # preview calendar-due beats; run and record nothing
```

`--dry` is deliberately narrower than a real fire: it previews the cadence
only. It does not take the beat lock or apply missed-run, overlap, cost, or
execution policy, so `would fire` is not a promise that the real firer will
run; `nika arm fire` can still answer `skipped` or refuse.

Startup, registry, clock, and loop failures exit `1`. Once the loop is
running, each beat's result is carried by its stdout line and ledger; a
completed `--once` sweep exits `0` even when an individual run failed or
paused. The hidden `--now` / `--until` pair injects the clock so a replay
stays deterministic.

<Warning>
  **The honesty contract.** Delivery is at-least-once and exactly-once
  is never claimed: a crash between the claim and the receipt leaves a
  visible orphan, not a silent double-fire. Keep a fired workflow's
  effects idempotent and the result is effectively-once.
</Warning>
