Skip to content

A task names what it serves, and the binding is derived, pinned and never stored

A task names what it serves, and the binding is derived, pinned and never stored

majordomus://adr/adr-0111proposed intentsessionhandovergovernance

it references

referenced by

Nothing in the layer names it. Backlinks are derived, so an empty list is an answer rather than an omission.

111. A task names what it serves, and the binding is derived, pinned and never stored

Context

ADR 0070 made an intent a typed record, ADR 0073 made an issue name the criteria it serves, and intent preflight joined the two: given an issue or some paths, which intent the work serves, whether it is maintenance, or why it may not proceed. Nothing asked it. start took a description and a scope; the task record named no issue and no intent; the context a worker was handed said nothing of the criteria the work was for; a handover carried a commit and no trace of the intent it was written under; and plan start moved an issue to ACTIVE on its status alone. Work on an intent whose plan was never critiqued was found by intent validate after it had begun, never refused before.

The owner asked for the loop to be closed at this end: a session that executes under an intent, with an explicit, typed, auditable exemption when work is legitimately under none.

Three things in force constrain how. Rule project.an-intent-outlives-its-sessions says no session, provider or handover holds intent state. ADR 0047 keeps the task lifecycle in the shell until it is cut over as a whole, and ADR 0069 says new deciding behaviour is a typed capability, not new shell. ADR 0052 says an episode is the provider's boundary and a task is an optional relation it carries, so nothing an episode needs may depend on a task.

Decision

One derivation. intent_binding::bind wraps the preflight and adds only what a task needs beside it: an intent given by name, an exemption, ambiguity, a scope note and two pins. It is answered by one read-only capability, intents.binding (majordomus-cli intent binding, GET /api/v1/intents/binding, majordomus_intent_binding). Its standing is bound, maintenance, exempt or refused, and every refusal carries a cause: the preflight's seven and nothing_named, unknown_intent, intent_retired, intent_has_no_open_work, issue_outside_intent, ambiguous_intent, unknown_exemption, exemption_without_reason, exemption_names_work.

The shell asks and decides nothing. start, context, handover, handover --resolve and plan start ask the built executable through lib/intent_binding.sh. An answer that cannot be read is unknown, and unknown is never a pass: each caller says why and what would settle it.

The policy decides what a refusal costs. intent.binding is off (or absent), advisory or required. Absent, a start with none of the new flags asks nothing and behaves as it always did, which is what an adopter whose policy predates the key gets. A flag is a question the worker put, so --issue, --intent and --exempt are asked under every value, and a refused or unreadable answer does not start. Under advisory every start asks and a refusal is reported. Under required a refused or unreadable binding does not start, in start and in plan start alike, in both plan engines.

An exemption is a declared class with a reason. intent.exemptions lists the classes; --exempt <class> --because "<reason>" is recorded in the task record and in the task.started ledger event. An undeclared class, a missing reason, and an exemption given beside named work are each refused. Derived maintenance — work under a milestone no intent names — needs no exemption, as intent validate already allows it. The absence of an intent is never an exemption by itself.

Paths alone that reach more than one intent are ambiguous. The worker names the issue.

A record keeps names and pins. The task record and a handover's front matter carry what the worker named and two hashes: plan_revision, over each served intent's statement, invariants, non-goals and criteria, the issues resolved with what each serves, and each critique's findings with their resolutions; and evidence_standing, over the evidence state of each served criterion. They are two because they answer different questions: an ordinary commit can make evidence stale without anyone having touched what the work is for. handover --resolve asks the binding again and says unchanged, evidence_moved or plan_changed, or unknown when it cannot ask. No stage, verdict, criterion or evidence state is stored anywhere; rule project.an-intent-outlives-its-sessions is amended to version 3 to say that a declared name and a hash compared on read are not intent state, and that the issue a task named is a declared link in the realization join.

The binding refuses a start; it does not judge a finish. Whether a task that claims completed has met the criteria it served is the completion question, which ADR 0107 keeps out of the intent's verdict and which a later decision settles.

Consequences

  • A repository that sets required must have the executable built: the shell cannot answer the binding itself, by design, and a required gate that passed when it could not read would not be a gate.
  • This repository sets advisory, and the measurement says why. The 221 distinct scopes its sessions gave start (every task.started event in the ledgers of the primary checkout and its worktrees, 2026-10-07) were each asked of intents.binding by paths alone against the plan at this change: 183 (83%) were refused — 180 as ambiguous_intent together with intent_not_critiqued, 3 as ambiguous_intent alone — 30 (14%) were bound and 8 (4%) were maintenance. A scope as broad as .ai/repo docs site overlaps issues of several intents, and intent-lifecycle has no critique. required would refuse five starts in six for naming a scope the way every session does today, so it waits until sessions name an issue as a habit and that intent's plan is critiqued; the number to watch is the refused share of starts that name nothing.
  • A critique is still judged by its presence and its open blocking findings. It records the commit it was written at and no revision of the plan it reviewed, so staleness has nothing to be compared with; the opposition milestone gives the critique record the plan_revision it reviewed, and its refusal reuses the hash computed here.
  • An episode is not bound. A provider session that edits with no task open is outside this decision, as ADR 0052 requires; the scope gate at check, finish and pre-push is what holds such work.
  • The task lifecycle gains shell code that asks, formats and refuses. It decides nothing, and moves to Rust with the rest of the lifecycle under ADR 0047.

Alternatives rejected

  • Store the resolved intent in the task record. It would end with the task, drift from the records the moment a criterion was edited, and contradict the rule that the lineage is derived. A pin costs one more read and cannot disagree with its source.
  • Store only the issue and derive the intent. A worker who names an intent directly would have nothing recorded, and the next reader could not tell "named an intent" from "named nothing".
  • One revision hash. It would report drift on every commit that changed a test's inputs, and a line that is always shown stops being read.
  • Refuse scope outside the named issue's scope. Issue scopes routinely omit the generated projections a change must regenerate; the binding says so in a note and the scope gate remains the task's own.
  • Gate only start. plan start and the plan.transition capability are the other way work begins, and a gate with a door beside it is advice.
  • Make required the default. Every adopter's start would begin refusing on upgrade for a reason their repository never declared.

Where this comes from

canonical file
.ai/repo/adrs/0111-a-task-names-what-it-serves-and-the-binding-is-derived.md
identity
adr-0111
also served as
MCP resource majordomus://adr/adr-0111 · majordomus entity show adr/adr-0111 · GET /api/v1/entity?uri=majordomus://adr/adr-0111
in the Cockpit
/cockpit/objects/adr/adr-0111

This page is generated from the file above by scripts/generate-site-data; editing the page changes nothing, editing the file changes every surface. The relations above are edges of docs/generated/graph.json, which the executable writes from the same index.

Searches the titles and descriptions of every page this site publishes.