Skip to content
Cockpit0.29.0
A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live

project.one-derive-at-a-time@1

One derive at a time per machine, and a stopped derive leaves no orphans

What it is

URI
majordomus://rule/project.one-derive-at-a-time@1
Kind
rule
Identity
project.one-derive-at-a-time@1
Path
.ai/repo/rules/project/one-derive-at-a-time.v1.md
Provenance
declared
Media type
text/markdown
As a capability
rule.project.one-derive-at-a-time@1

Front matter

{
  "id": "project.one-derive-at-a-time",
  "version": 1,
  "kind": "rule",
  "title": "One derive at a time per machine, and a stopped derive leaves no orphans",
  "description": "scripts/derive holds the machine's derive lock for its whole run, yields to a priority marker that names another branch, reclaims a dead owner's lock aloud, and takes its whole process tree down when it is stopped.",
  "statement": "Run every derive, and every full suite, through scripts/derive (`scripts/derive --locked -- <command>` for a suite), never around it. The script takes the machine's lock, waits for a live owner and for a priority marker that names another branch, and on TERM, INT or HUP stops every process it started.",
  "status": "active",
  "class": "blocking",
  "depends_on": [],
  "tags": [
    "derived",
    "machine",
    "coordination"
  ],
  "x-majordomus": {
    "tests": [
      "test/cases/1018_one_derive_at_a_time_and_no_orphans.sh"
    ]
  }
}

Content

---
id: project.one-derive-at-a-time
version: 1
kind: rule
title: One derive at a time per machine, and a stopped derive leaves no orphans
description: scripts/derive holds the machine's derive lock for its whole run, yields to a priority marker that names another branch, reclaims a dead owner's lock aloud, and takes its whole process tree down when it is stopped.
statement: Run every derive, and every full suite, through scripts/derive (`scripts/derive --locked -- <command>` for a suite), never around it. The script takes the machine's lock, waits for a live owner and for a priority marker that names another branch, and on TERM, INT or HUP stops every process it started.
status: active
class: blocking
depends_on: []
tags: [derived, machine, coordination]

x-majordomus:
  tests: [test/cases/1018_one_derive_at_a_time_and_no_orphans.sh]
---

# Rationale

A derive rewrites every committed artifact. Under load it costs from minutes to an hour of
one machine shared by every session working this repository. On 2026-10-09 nine sessions
shared it at load 57. They serialised their derives through a lock directory and a priority
marker beside the primary checkout, but the convention lived only in the wrappers they wrote
around `scripts/derive`. One `just derive` without the wrapper ran beside a repair's locked
derive. Stopping it killed the shell and not stage B: bash defers a trap until a foreground
child returns. So `generate-site-data` ran on for an hour, reparented to pid 1, with nobody's
name on it. The integrator first blamed the wrong session for the lock it saw held.

A convention every worker has to remember is a convention one worker forgets. The script
that does the expensive work is the one place every derive passes through, so the lock and
the clean-up belong there. They live in `lib/machine_lock.sh`, which `scripts/derive`
sources, so the next script that needs the machine can source it too. d1 built the same lock
independently (`fix/a-derive-takes-the-machine-lock`) and retired it to review this one. Its
reading of owner lines without a pid is the one kept here.

# Required behaviour

- `scripts/derive` takes the lock before it builds anything. It is a directory made
  atomically by `mkdir`, with an owner file of `<tag> <pid>`. The default is
  `.derive.lock` beside the primary checkout, where the sessions kept it, derived from git's
  common directory and never written as a machine path. `MAJORDOMUS_DERIVE_LOCK` overrides it.
- The holder's pid is the owner line's `pid=<n>`, or its last word, and only when that is a
  number. A line with no numeric pid, such as a hand-taken lock or a loop that ended its line
  with a time, is a live holder that cannot be checked. It is never reclaimed and never taken
  for an ancestor. It is waited for, and the wait prints the line.
- It is re-entrant: when the owner's pid is an ancestor of the derive, the caller holds the
  lock and the derive runs under it, leaving it to the caller.
- A lock whose numeric owner pid is not running is reclaimed. The reclaim prints the dead
  owner's line. It is never silent.
- Only the process that wrote the owner line releases the lock, and only while the line is
  still exactly what it wrote.
- While `.derive.priority` beside the lock exists and does not name the derive's branch as a
  whole word, the lock is not taken. A marker naming `feature/x-two` does not name
  `feature/x`. `MAJORDOMUS_DERIVE_IGNORE_PRIORITY=1` is for the marker's own author,
  whose derive runs under another name.
- Every stage runs as a background child under `wait`, so a signal is handled at once. On
  TERM, INT or HUP the derive signals every descendant deepest first, kills the survivors,
  releases the lock and exits 143, 130 or 129.
- A derive that finds an orphan of an earlier derive of this checkout (parent pid 1, command
  under this checkout's `scripts/derive` or `scripts/generate-site-data`) stops it and its tree
  before it starts, naming it. This follows `project.reclaim-only-what-you-own`: only what
  this checkout's own derive started, matched by the absolute script path and the dead
  parent, never by a process name. `scripts/reap-orphans` keeps the same discipline for
  servers and build output.
- `scripts/derive --locked -- <command>` runs a command, such as a full suite, under the same
  lock and the same signal handling.

# Failure behaviour

A derive that waits says why, once per reason: who holds the lock, or what the priority
marker says. It gives up with exit 75 after `MAJORDOMUS_DERIVE_WAIT_MAX` seconds (default
14 400). A derive started around the script, by calling its stages directly, is outside this
rule's enforcement, and the rule's statement forbids it.

# Verification

`test/cases/1018_one_derive_at_a_time_and_no_orphans.sh` drives the lock through `--locked`
and proves, each by observation, these behaviours:

- taking and releasing the lock;
- reclaiming a dead owner's lock with the owner's name in the output, for both owner-line forms;
- never reclaiming an owner line with no numeric pid, including today's hand-written and
  time-stamped lines;
- release only by the process whose line it still is;
- waiting for a live owner without touching its lock, and giving up with exit 75;
- waiting for a marker that names another branch, or a longer branch with this one as a
  prefix, and running for one that names this one;
- re-entry under the old wrapper;
- a TERM that leaves no grandchild running and no lock behind;
- an orphan predicate, over a process table the case writes, that selects exactly this
  checkout's ppid-1 derives and never another worktree's, a prefix sibling's, a nested path's
  or a live derive's.

Each of these mutations fails the case: the tree kill removed, the reclaim notice removed,
the numeric-pid check removed, the whole-word marker match turned into a substring match, the
owner check on release removed, and the path's word boundary removed. The reaper's kill
itself has no live proof: a real ppid-1 process cannot be manufactured portably inside a
test, so its predicate is proved over the fixture instead.