01 — The Verb and the Run
Status: resolved + shipped (2026-07-17). Companion to Sense 55 (The Broadcast) and Sense 56 (The Run).
The question
Section titled “The question”Sense 56’s title is “Verbs and Runs as nouns” — plural, all Verbs. But
Run.open had exactly one call site in the engine: inside make. A
snapshot froze an immutable artifact, an afs update rewrote the tenant’s
analytical framework, an invite granted a human authority — and none of them
left a record. Meanwhile make carried a full manifest, an event stream, and
(since the trigger-provenance work) the name of who fired it.
In DDD terms: we modeled the aggregate correctly and then wired it into a single use case. The Run is an aggregate of the operational context — “executions of Verbs against a tenant” — and its invariant should read:
Being a state-changing Verb means leaving a Run.
The asymmetry was becoming a security-adjacent gap: the console stamps
trigger.actor on a make, while an afs update --force — arguably the more
dangerous act — was invisible in /runs.
The shape question
Section titled “The shape question”Two candidate homes for Run-opening:
- In each Verb (the status quo, repeated N times) — each command opens its own Run where it knows its own tenant and its own phases.
- In the dispatch (once, uniformly) — the CLI’s single
args.func(args)call site brackets the Verb in a Run scope.
The dispatch answer is the DDD-honest one: the invariant belongs to the
application layer, not to any one use case. A Verb author should get the
record for free, the way make gets run.started/run.finished from its
bracket today. We chose the dispatch — with one deliberate exception below.
Decisions
Section titled “Decisions”D1 — A registry, not an inference. RUN_SCOPED_VERBS is an explicit set:
snapshot, invite, and the mutating afs subcommands (update, promote,
reset, pull, push, pp, merge, checkout). Read-only verbs (ls,
inspect, afs status, afs diff, …) leave no Run — a record of looking is
noise. Growing the set is a one-line, reviewable act.
D2 — make keeps its interior Run. make opens its Run after
pre-flight validation, deliberately (“the Run represents an execution that
actually enters the pipeline”), and owns a phase DAG the dispatch cannot know.
The dispatch scope skips make; the invariant is satisfied by its existing
bracket. Uniformity of the record matters more than uniformity of the
mechanism.
D3 — init stays out, on principle. init creates the bounded
context. A tenant’s runs/ directory is the tenant’s own record, and it
cannot record its own birth — the Run would have to be written into a
directory the Verb is still creating, by an actor the tenant cannot yet know.
The record of tenant creation is a fleet-level fact and belongs to the
Reservoir (Sense 61) when it lands. Recording it in the tenant would be a
modeling lie for the sake of coverage.
D4 — One tenant, one Run. A Run is a per-tenant record (it lives in
<live>/<pack>/<tenant>/runs/). Phase 1 brackets only invocations that
resolve to exactly one tenant; multi-target forms (afs update a b,
--all) run unrecorded for now. The honest future shape is one Run per
resolved tenant (a fleet invocation fans out into N tenant-level facts, tied
by parent_run_id), which needs the fleet-level record of D3 first.
D5 — Verb names are dotted. An afs update Run records
verb.name: "afs.update" — identifier-tier (id-safe, greppable), while the
display tier is free to render it however reads best.
D6 — Failure carries its why. The scope reuses the Broadcast error
discipline: an exception escaping the Verb emits run.error (StepFailure
detail preserved; traceback tail for the unexpected) before run.finished.
Same events, same replay page, no new machinery.
D7 — Best-effort, never load-bearing. Exactly like make’s
instrumentation: a Run that cannot open (unresolvable tenant, read-only disk,
missing live root) degrades to running the Verb bare. The scope must never
change a Verb’s behavior, exit code, or output. Tenant resolution reuses the
same resolver the Verbs themselves use, and swallows everything — including
the resolver’s own sys.exit on “no default tenant”, which the Verb will then
raise itself, visibly, as it always did.
What this buys
Section titled “What this buys”/runsbecomes the complete operational record for a tenant: every make, snapshot, framework mutation, and invitation, each with status, duration, trigger, and actor.- The trigger-provenance work generalizes:
--run-id,--trigger-kind,--trigger-actorare accepted by every run-scoped Verb, so the browser console and the worker daemon stamp WHO on all of them, not justmake. - Slice 3 (the ambient feed) gets a uniform substrate: “runs anyone started” now means any Verb, not any make.
The line we drew
Section titled “The line we drew”“Being a Verb means leaving a Run” was the candidate invariant; the shipped one is narrower and truer: being a state-changing Verb against an existing tenant means leaving a Run in that tenant. Creation events belong to the fleet’s record; reads belong to no record at all.