# `conflicts` (Service Mutual Exclusion) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a `conflicts` field to the `[dependencies]` block so two services can be declared mutually exclusive — at boot, both-enabled conflicting services fail; at runtime, starting a service whose conflicting peer is running is refused.

**Architecture:** Two non-overlapping enforcement points funnel through known code. A boot-only pre-flight (`resolve_conflicts` in `main.reef`) marks both members of any active conflicting pair `FAILED` before any fork. A runtime guard inside `supervisor.start_service` (the single choke point for every start) refuses a start when a conflicting peer is `RUNNING`/`STARTING`. A symmetric helper `conflicts_with` backs both. `conflicts` is mutual exclusion, not ordering — it never enters `depgraph.reef`.

**Tech Stack:** Reef (`.reef` → C → native via `reefc`); POSIX shell integration tests; Linux dev build (clang + contract stubs). No Hammerhead VM required.

**Spec:** `docs/superpowers/specs/2026-06-13-conflicts-design.md`

---

## Reef / build orientation (read once)

- Reef structs are **value types**: to mutate a runtime you read it, change fields, write it back — e.g. `let rt = table.runtimes[idx]` … `rt.state = …` … `table.runtimes[idx] = rt`. See `start_service` at `src/supervisor.reef:327` for the canonical pattern.
- Blocks close with `end <kw>` (`end if`, `end fn`, `end while`, `end proc`). `fn` returns a value; `proc` does not. `mut` = mutable, `let` = immutable.
- String building uses `core.str` — `str.concat(a, b)` (already imported as `str` in `supervisor.reef`). `socket.reef` builds strings with the `+` operator (both work; match the file you're editing).
- **Build (Linux dev):**
  ```bash
  clang -c src/helpers.c -o build/helpers.o
  clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
  reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
  ```
  zygctl (needed for the integration suite):
  ```bash
  cd tools/zygctl && clang -c src/symlink_wrapper.c -o build/symlink_wrapper.o && reefc build --obj build/symlink_wrapper.o && cd ../..
  ```
- **Run the integration suite:** `./tests/integration/run_tests.sh`
- **VCS is Mercurial.** Commit with `hg commit -m "..."`. New files need `hg add <path>` first. Every source file must carry the `SRCHEADER.txt` header — but in this plan we only *edit* existing `.reef` files (which already have headers) and add a shell test + docs, so no new `.reef` headers are introduced.

---

## File Map

| File | Change |
|------|--------|
| `src/config.reef` | Add `conflicts`/`conflicts_count` to `ServiceDef`, init, accessors, TOML parse branch |
| `src/supervisor.reef` | `conflicts_with`, `conflicting_running_peer`, `service_name_at`, `mark_conflict_failed`; runtime guard in `start_service` |
| `src/main.reef` | `resolve_conflicts` + `warn_unknown_conflicts` procs; call before boot tier-start; skip non-`WAITING` services in `start_services_by_tier` |
| `src/ui_render.reef` | Show the stored reason for `FAILED` rows in `zygctl status` |
| `src/socket.reef` | `cmd_start` returns the specific conflict message |
| `tests/integration/run_tests.sh` | `create_service_with_conflicts` helper + new "Service conflicts" suite |
| `docs/DESIGN.md`, `docs/man/man5/zyginit.5`, `CLAUDE.md`, `ROADMAP.md` | Document `conflicts`; check roadmap box |

---

## Task 1: Parse `conflicts` and expose accessors (`config.reef`)

Pure data plumbing — no observable behavior yet, so its "test" is: the project still builds and the existing suite stays green. (The repo has no Reef unit-test harness; `conflicts` parsing is verified end-to-end by Task 2's boot test, which cannot pass unless the array parses correctly.)

**Files:**
- Modify: `src/config.reef` (struct ~178, init ~219, accessors ~302, parser ~614)

- [ ] **Step 1: Add struct fields**

In `type ServiceDef = struct` (`src/config.reef`), immediately after:
```
    after: [string]
    after_count: int
```
add:
```
    conflicts: [string]
    conflicts_count: int
```

- [ ] **Step 2: Initialize in `new_service_def()`**

In the `ServiceDef{ ... }` literal, immediately after:
```
        after: new [string](16),
        after_count: 0,
```
add:
```
        conflicts: new [string](16),
        conflicts_count: 0,
```

- [ ] **Step 3: Add accessors**

After the `svc_after_count` accessor, add:
```
fn svc_conflicts(svc: ServiceDef): [string]
    return svc.conflicts
end svc_conflicts

fn svc_conflicts_count(svc: ServiceDef): int
    return svc.conflicts_count
end svc_conflicts_count
```

- [ ] **Step 4: Add TOML parse branch**

In the `[dependencies]` parsing area, immediately after the `dependencies.after` block:
```
    if toml.toml_has_key(keys, vals, count, "dependencies.after")
        let after_str = toml.toml_get(keys, vals, count, "dependencies.after")
        svc.after_count = parse_toml_array(after_str, svc.after, 16)
    end if
```
add:
```
    if toml.toml_has_key(keys, vals, count, "dependencies.conflicts")
        let conf_str = toml.toml_get(keys, vals, count, "dependencies.conflicts")
        svc.conflicts_count = parse_toml_array(conf_str, svc.conflicts, 16)
    end if
```

- [ ] **Step 5: Build**

Run:
```bash
clang -c src/helpers.c -o build/helpers.o
clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
```
Expected: build succeeds, `build/zyginit` produced.

- [ ] **Step 6: Verify no regression**

Run: `./tests/integration/run_tests.sh`
Expected: same pass count as before this task (all green). If zygctl isn't built yet, build it per the orientation block first.

- [ ] **Step 7: Commit**

```bash
hg commit src/config.reef -m "config: parse [dependencies] conflicts array"
```

---

## Task 2: Boot-time resolution — fail both (`supervisor.reef` + `main.reef` + `ui_render.reef`)

First observable behavior. TDD: add the boot test (red), implement, go green.

**Files:**
- Modify: `tests/integration/run_tests.sh` (helper + new suite)
- Modify: `src/supervisor.reef` (`conflicts_with`, `mark_conflict_failed`, `service_name_at`)
- Modify: `src/main.reef` (`resolve_conflicts`, `warn_unknown_conflicts`, call site, tier-start state guard)
- Modify: `src/ui_render.reef` (status reason for `FAILED`)

- [ ] **Step 1: Add the test helper**

In `tests/integration/run_tests.sh`, after the `create_service_with_env()` helper, add:
```bash
# Create a service with a [dependencies] conflicts array.
# Usage: create_service_with_conflicts <name> <type> <cmd> "\"other\""
create_service_with_conflicts() {
    local name="$1"
    local type="$2"
    local cmd="$3"
    local conflicts="$4"

    mkdir -p "$CONFIG_DIR/services/$name"
    cat > "$CONFIG_DIR/services/$name/service.toml" << TOML
[service]
description = "Test service: $name"
type = "$type"

[exec]
start = "$cmd"

[dependencies]
conflicts = [$conflicts]

[restart]
on = "never"
TOML
}
```

- [ ] **Step 2: Add the boot-resolution test section**

Append a new section near the other suites (after the "Restart policies" section). Use a fresh config dir like the other sections do:
```bash
# ============================================================================
# Test Suite: Service conflicts
# ============================================================================
section "Service conflicts"

rm -rf "$CONFIG_DIR"
mkdir -p "$CONFIG_DIR" "$ENABLED_DIR"

# Two daemons that declare each other as conflicting; both enabled.
create_service_with_conflicts "mailer-a" "daemon" "$BIN_DIR/sleeper.sh" "\"mailer-b\""
create_service "mailer-b" "daemon" "$BIN_DIR/sleeper.sh"
enable_service "mailer-a"
enable_service "mailer-b"

start_daemon

# Boot resolution: both enabled + conflicting => start neither, fail both.
output=$(zygctl status mailer-a)
assert_contains "$output" "failed" "conflict: mailer-a failed at boot"
assert_contains "$output" "conflicts with" "conflict: mailer-a status shows reason"
output=$(zygctl status mailer-b)
assert_contains "$output" "failed" "conflict: mailer-b failed at boot"

stop_daemon
```

- [ ] **Step 3: Run the suite — confirm the new asserts FAIL**

Run: `./tests/integration/run_tests.sh`
Expected: the four new `conflict:` asserts FAIL (both services currently start and report `running`, never `failed`). Existing tests still pass.

- [ ] **Step 4: Add `conflicts_with` to `supervisor.reef`**

After `service_count` (`src/supervisor.reef:294`), add:
```
// True if services a and b are declared mutually exclusive. Symmetric: a
// conflict declared on either side counts for both. Self-comparison and
// unknown names never match.
fn conflicts_with(table: ServiceTable, a_idx: int, b_idx: int): bool
    if a_idx == b_idx
        return false
    end if
    let a_def = table.runtimes[a_idx].def
    let b_def = table.runtimes[b_idx].def
    let a_name = config.svc_name(a_def)
    let b_name = config.svc_name(b_def)

    let a_conf = config.svc_conflicts(a_def)
    let a_n = config.svc_conflicts_count(a_def)
    mut i = 0
    while i < a_n
        if a_conf[i] == b_name
            return true
        end if
        i = i + 1
    end while

    let b_conf = config.svc_conflicts(b_def)
    let b_n = config.svc_conflicts_count(b_def)
    i = 0
    while i < b_n
        if b_conf[i] == a_name
            return true
        end if
        i = i + 1
    end while

    return false
end conflicts_with
```

- [ ] **Step 5: Add `service_name_at` and `mark_conflict_failed` to `supervisor.reef`**

Directly after `conflicts_with`, add:
```
// Name of the service at idx (convenience for callers outside this module).
fn service_name_at(table: ServiceTable, idx: int): string
    return config.svc_name(table.runtimes[idx].def)
end service_name_at

// Mark a service FAILED because of a conflict, recording the reason for
// `zygctl status`. The skip_reason field doubles as a generic status reason.
proc mark_conflict_failed(table: ServiceTable, idx: int, other_name: string)
    let rt = table.runtimes[idx]
    rt.state = STATE_FAILED()
    rt.skip_reason = str.concat("conflicts with ", other_name)
    table.runtimes[idx] = rt
end mark_conflict_failed
```

- [ ] **Step 6: Add `resolve_conflicts` + `warn_unknown_conflicts` to `main.reef`**

In `src/main.reef`, after the `start_services_by_tier` proc (ends at `:366`), add:
```
// Warn (once, at boot) about conflict targets that name a non-existent service.
proc warn_unknown_conflicts(table: supervisor.ServiceTable, def: config.ServiceDef)
    let conf = config.svc_conflicts(def)
    let n = config.svc_conflicts_count(def)
    mut k = 0
    while k < n
        if supervisor.find_service(table, conf[k]) < 0
            println("zyginit: warning: " + config.svc_name(def) +
                    " conflicts with unknown service: " + conf[k])
        end if
        k = k + 1
    end while
end warn_unknown_conflicts

// Boot-only pre-flight: for every pair of services that are both active in the
// current runlevel and declared mutually exclusive, mark BOTH failed before any
// fork. zyginit never auto-picks a winner. Runtime conflicts (zygctl start,
// runlevel transitions) are handled by the guard in supervisor.start_service.
proc resolve_conflicts(table: supervisor.ServiceTable)
    let n = supervisor.service_count(table)
    mut i = 0
    while i < n
        let rt_i = supervisor.get_runtime(table, i)
        let def_i = supervisor.rt_def(rt_i)
        if supervisor.rt_state(rt_i) == supervisor.STATE_WAITING() and service_in_runlevel(def_i, g_runlevel)
            warn_unknown_conflicts(table, def_i)
            mut j = i + 1
            while j < n
                let rt_j = supervisor.get_runtime(table, j)
                let def_j = supervisor.rt_def(rt_j)
                if supervisor.rt_state(rt_j) == supervisor.STATE_WAITING() and service_in_runlevel(def_j, g_runlevel) and supervisor.conflicts_with(table, i, j)
                    let name_i = config.svc_name(def_i)
                    let name_j = config.svc_name(def_j)
                    supervisor.mark_conflict_failed(table, i, name_j)
                    supervisor.mark_conflict_failed(table, j, name_i)
                    if ui.ui_mode() == ui.MODE_PLAIN()
                        println("zyginit: conflict: " + name_i + " and " + name_j +
                                " are mutually exclusive; failing both")
                    end if
                end if
                j = j + 1
            end while
        end if
        i = i + 1
    end while
end resolve_conflicts
```
(`main.reef` already imports `config`, `supervisor`, and `ui`, and defines `service_in_runlevel` at `:187` and `g_runlevel` at `:167`.)

- [ ] **Step 7: Call `resolve_conflicts` before the boot tier-start**

In `src/main.reef`, in the "Phase 6: Start services" block, the boot call is at `:1093`:
```
        ui.ui_boot_start(svc_count_clamped, num_tiers, runlevel_name)
        start_services_by_tier(table, tiers, tier_counts, num_tiers)
```
Insert the pre-flight between those two lines:
```
        ui.ui_boot_start(svc_count_clamped, num_tiers, runlevel_name)
        resolve_conflicts(table)
        start_services_by_tier(table, tiers, tier_counts, num_tiers)
```
Do NOT add it to the runlevel-transition reuse of `start_services_by_tier` (`:862`); transitions are runtime and rely on the `start_service` guard (Task 3).

- [ ] **Step 8: Make the tier-start loop skip already-FAILED services**

In `start_services_by_tier` (`src/main.reef:278`), the start condition currently is:
```
                if service_in_runlevel(def, g_runlevel) and state != supervisor.STATE_RUNNING()
                    supervisor.start_service(table, idx)
```
A conflict-failed service has `state == FAILED` (not `RUNNING`), so it would otherwise be (re)started. Tighten the condition to only start `WAITING` services:
```
                if service_in_runlevel(def, g_runlevel) and state == supervisor.STATE_WAITING()
                    supervisor.start_service(table, idx)
```
This is safe for the runlevel-transition reuse: newly-active services are `WAITING` (or `mark_runlevel_filtered`), and the existing `elif ... state == STATE_WAITING()` branch below is unaffected.

- [ ] **Step 9: Show the stored reason for FAILED rows in status (`ui_render.reef`)**

In `src/ui_render.reef`, the notes block in `fmt_status_row` (`:168`) currently begins:
```
    if state == supervisor.STATE_SKIPPED()
        let reason = supervisor.rt_skip_reason(rt)
        if str.length(reason) > 0
            notes = reason
        end if
    elif last_exit >= 0 and state != supervisor.STATE_RUNNING() and state != supervisor.STATE_WAITING()
```
Replace the first `if` branch so a `FAILED` service with a stored reason shows it too:
```
    let reason = supervisor.rt_skip_reason(rt)
    if state == supervisor.STATE_SKIPPED() and str.length(reason) > 0
        notes = reason
    elif state == supervisor.STATE_FAILED() and str.length(reason) > 0
        notes = reason
    elif last_exit >= 0 and state != supervisor.STATE_RUNNING() and state != supervisor.STATE_WAITING()
```
(A normally-failed daemon has `reason == ""`, so it still falls through to the `exit=` branch.)

- [ ] **Step 10: Build**

Run:
```bash
clang -c src/helpers.c -o build/helpers.o
clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
```
Expected: build succeeds.

- [ ] **Step 11: Run the suite — boot asserts now PASS**

Run: `./tests/integration/run_tests.sh`
Expected: the four `conflict:` boot asserts now PASS; all previously-passing tests still pass.

- [ ] **Step 12: Commit**

```bash
hg add tests/integration/run_tests.sh
hg commit src/supervisor.reef src/main.reef src/ui_render.reef tests/integration/run_tests.sh \
    -m "supervisor: boot-time conflict resolution (fail both); status shows reason"
```

---

## Task 3: Runtime guard — refuse (`supervisor.reef` + `socket.reef`)

**Files:**
- Modify: `tests/integration/run_tests.sh` (extend the "Service conflicts" section)
- Modify: `src/supervisor.reef` (`conflicting_running_peer` + guard in `start_service`)
- Modify: `src/socket.reef` (`cmd_start` specific message)

- [ ] **Step 1: Add the runtime-refusal test**

In `tests/integration/run_tests.sh`, inside the "Service conflicts" section, **before** the final `stop_daemon`, append a fresh-config runtime scenario:
```bash
# --- Runtime refusal: starting a service whose conflicting peer runs is refused.
stop_daemon
rm -rf "$CONFIG_DIR"
mkdir -p "$CONFIG_DIR" "$ENABLED_DIR"

# Only svc-a is enabled at boot; svc-b conflicts with it and is started by hand.
create_service "svc-a" "daemon" "$BIN_DIR/sleeper.sh"
create_service_with_conflicts "svc-b" "daemon" "$BIN_DIR/sleeper.sh" "\"svc-a\""
enable_service "svc-a"
enable_service "svc-b"
# Disable svc-b's autostart by NOT enabling it... but boot resolution would fail
# both if both enabled. Instead enable only svc-a; add svc-b via reload-less path:
disable_service "svc-b"

start_daemon

output=$(zygctl status svc-a)
assert_contains "$output" "running" "runtime: svc-a is running"

# Enable svc-b and reload so it joins the table as WAITING, then try to start it.
enable_service "svc-b"
zygctl reload > /dev/null 2>&1
sleep 1
output=$(zygctl start svc-b)
assert_contains "$output" "conflicts with running service" "runtime: start svc-b refused"
output=$(zygctl status svc-a)
assert_contains "$output" "running" "runtime: svc-a still running after refusal"

stop_daemon
```
Note: `reload` adds the newly-enabled `svc-b` as `WAITING` without auto-starting it through the boot pre-flight, which is exactly the runtime path we want to exercise. If `reload` auto-starts new services in this codebase, the `zygctl start svc-b` still exercises the guard because the start is attempted while `svc-a` runs; assert on the refusal message regardless.

- [ ] **Step 2: Run the suite — runtime asserts FAIL**

Run: `./tests/integration/run_tests.sh`
Expected: the two new `runtime:` asserts FAIL (`start svc-b` currently succeeds).

- [ ] **Step 3: Add `conflicting_running_peer` to `supervisor.reef`**

Directly after `conflicts_with` (added in Task 2), add:
```
// Index of a RUNNING/STARTING service that conflicts with idx, or -1 if none.
// Shared by the start guard and by zygctl start for messaging.
fn conflicting_running_peer(table: ServiceTable, idx: int): int
    mut i = 0
    while i < table.count
        if i != idx and conflicts_with(table, idx, i)
            let st = table.runtimes[i].state
            if st == STATE_RUNNING() or st == STATE_STARTING()
                return i
            end if
        end if
        i = i + 1
    end while
    return 0 - 1
end conflicting_running_peer
```

- [ ] **Step 4: Add the guard inside `start_service`**

In `start_service` (`src/supervisor.reef`), immediately after the `[condition]` evaluation block closes (`end if` at `:341`) and **before** `ui.ui_event_starting(name)` (`:344`), insert:
```
    // Conflict guard: refuse to start when a mutually-exclusive peer is already
    // running or starting. Leaves this service's state unchanged. Covers
    // zygctl start, runlevel transitions, and restarts (all funnel here).
    let conflict_peer = conflicting_running_peer(table, idx)
    if conflict_peer >= 0
        let other = config.svc_name(table.runtimes[conflict_peer].def)
        if ui.ui_mode() == ui.MODE_PLAIN()
            println(str.concat(str.concat("supervisor: cannot start ", name),
                    str.concat(": conflicts with running service ", other)))
        end if
        return false
    end if
```

- [ ] **Step 5: Return the specific message from `cmd_start` (`socket.reef`)**

In `src/socket.reef`, in `cmd_start`, after the `already running` check and before `if supervisor.start_service(table, idx)` (`:318`), insert:
```
    let conflict_peer = supervisor.conflicting_running_peer(table, idx)
    if conflict_peer >= 0
        let other = supervisor.service_name_at(table, conflict_peer)
        return "error: cannot start " + name + ": conflicts with running service " + other + "\n"
    end if
```

- [ ] **Step 6: Build**

Run:
```bash
clang -c src/helpers.c -o build/helpers.o
clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
```
Expected: build succeeds.

- [ ] **Step 7: Run the suite — runtime asserts now PASS**

Run: `./tests/integration/run_tests.sh`
Expected: the `runtime:` asserts now PASS; everything else still green.

- [ ] **Step 8: Commit**

```bash
hg commit src/supervisor.reef src/socket.reef tests/integration/run_tests.sh \
    -m "supervisor: refuse start when a conflicting peer is running"
```

---

## Task 4: Symmetry verification

The symmetric relation is already implemented in `conflicts_with` (it checks both directions). This task adds a regression test proving one-sided declaration is enforced both ways — no implementation change expected.

**Files:**
- Modify: `tests/integration/run_tests.sh` (extend the "Service conflicts" section)

- [ ] **Step 1: Add the symmetry test**

In the "Service conflicts" section, before its final `stop_daemon`, append:
```bash
# --- Symmetry: declare the conflict on ONE side only; both still fail at boot.
stop_daemon
rm -rf "$CONFIG_DIR"
mkdir -p "$CONFIG_DIR" "$ENABLED_DIR"

# only one-side declares the conflict
create_service_with_conflicts "sym-a" "daemon" "$BIN_DIR/sleeper.sh" "\"sym-b\""
create_service "sym-b" "daemon" "$BIN_DIR/sleeper.sh"
enable_service "sym-a"
enable_service "sym-b"

start_daemon

output=$(zygctl status sym-a)
assert_contains "$output" "failed" "symmetry: sym-a failed (declared side)"
output=$(zygctl status sym-b)
assert_contains "$output" "failed" "symmetry: sym-b failed (non-declared side)"

stop_daemon
```

- [ ] **Step 2: Run the suite — symmetry asserts PASS**

Run: `./tests/integration/run_tests.sh`
Expected: both `symmetry:` asserts PASS with no code change (the boot section's `mailer-*` services already declared one-sided, but this makes symmetry explicit). All tests green.

- [ ] **Step 3: Commit**

```bash
hg commit tests/integration/run_tests.sh \
    -m "test: conflicts symmetry — one-sided declaration enforced both ways"
```

---

## Task 5: Documentation

**Files:**
- Modify: `docs/DESIGN.md` (`:103-105`), `docs/man/man5/zyginit.5` (`:160`), `CLAUDE.md` (Service Definition Schema `[dependencies]`), `ROADMAP.md` (`:150`)

- [ ] **Step 1: `docs/DESIGN.md` schema reference**

In the `[dependencies]` example (`:103-105`):
```
[dependencies]
requires = ["network", "filesystem"]
after = ["name-services"]
```
add a `conflicts` line:
```
[dependencies]
requires = ["network", "filesystem"]
after = ["name-services"]
conflicts = ["other-mta"]    # mutually exclusive; never run together
```

- [ ] **Step 2: `zyginit.5` man page**

In `docs/man/man5/zyginit.5`, the `[dependencies]` list ends at `:160-161`:
```
.Dl after = ["syslog", "name-services"]
.El
```
Insert a `conflicts` entry before `.El`:
```
.Dl after = ["syslog", "name-services"]
.It Sy conflicts Pq array of strings
Services that must never run at the same time as this one.
The relation is symmetric: declaring the conflict on either service is enough.
If two conflicting services are both enabled at boot,
.Em both
are marked failed (zyginit never auto-selects a winner).
At runtime,
.Xr zygctl 8
.Cm start
is refused if a conflicting service is already running.
.Pp
Example:
.Dl conflicts = ["sendmail"]
.El
```

- [ ] **Step 3: `CLAUDE.md` quick-reference schema**

In `CLAUDE.md`, the Service Definition Schema `[dependencies]` block:
```
[dependencies]
requires = ["svc1", "svc2"]  # must be running
after = ["svc3"]             # ordering only (not hard dependency)
```
add:
```
[dependencies]
requires = ["svc1", "svc2"]  # must be running
after = ["svc3"]             # ordering only (not hard dependency)
conflicts = ["svc4"]         # mutually exclusive — both fail if both enabled; start refused if peer running
```

- [ ] **Step 4: `ROADMAP.md` — check the box**

At `ROADMAP.md:150`, change:
```
- [ ] **`conflicts`** — prevent two services from running simultaneously
      (e.g., sendmail vs postfix)
```
to:
```
- [x] **`conflicts`** — prevent two services from running simultaneously
      (e.g., sendmail vs postfix). Symmetric; both fail if both enabled at
      boot, runtime start refused if a conflicting peer is running.
```

- [ ] **Step 5: Commit**

```bash
hg commit docs/DESIGN.md docs/man/man5/zyginit.5 CLAUDE.md ROADMAP.md \
    -m "docs: document [dependencies] conflicts field"
```

---

## Final verification

- [ ] **Full suite green**

Run: `./tests/integration/run_tests.sh`
Expected: all suites pass, including the new "Service conflicts" section (boot fail-both, runtime refusal, symmetry). Note the new total in the summary line.

- [ ] **Clean build from scratch**

Run:
```bash
rm -f build/zyginit
clang -c src/helpers.c -o build/helpers.o
clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
```
Expected: `build/zyginit` builds with no warnings about `conflicts`.

---

## Notes / decisions carried from the spec

- **Two mechanisms, non-overlapping:** after `resolve_conflicts` runs at boot, no conflicting pair both survive as `WAITING`, so the `start_service` guard never trips spuriously during the boot tier loop.
- **Runlevel transitions** use the runtime guard (refuse), not the boot pre-flight — consistent with the "refuse at runtime" semantic.
- **`skip_reason` reuse:** the field stores the conflict reason for `FAILED` services. Kept the field name to minimize diff; `ui_render` now surfaces it for `FAILED` as well as `SKIPPED`. If a future change wants a cleaner name, rename `skip_reason` → `status_reason` across `supervisor.reef` + `ui_render.reef` in one pass.
- **Cascade is intended:** a service that `requires` a conflict-failed service will itself fail to start (existing dependency behavior). Documented, not worked around.
- **`conflicts` is never a graph edge** — `depgraph.reef` is untouched.
