Skip to content

01 — The Verb and the Run

Status: resolved + shipped (2026-07-17). Companion to Sense 55 (The Broadcast) and Sense 56 (The Run).

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.

Two candidate homes for Run-opening:

  1. 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.
  2. 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.

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.

  • /runs becomes 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-actor are accepted by every run-scoped Verb, so the browser console and the worker daemon stamp WHO on all of them, not just make.
  • Slice 3 (the ambient feed) gets a uniform substrate: “runs anyone started” now means any Verb, not any make.

“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.

jazzisnow jinflow is a jazzisnow product
v0.64.7 · built 2026-09-20 19:48 UTC