Skip to content

HTTP API: obligations

What a task owes before it may be called completed, and whether the evidence that discharged each obligation still describes this tree. The vocabulary is data the distribution ships and answers in any clone; the closure is read from the local half of the layer, which this process serves to the worker in front of it and never publishes. Read, never written: `majordomus evidence` records, and a second writer for one ledger would be a second account of the same events.

Rendered from docs/generated/openapi.json, which the executable generates from its capability registry and CI refuses stale. The raw document is at /openapi.json; a running server serves the same document at its own /openapi.json and Swagger UI at /swagger.

obligations

What a task owes before it may be called completed, and whether the evidence that discharged each obligation still describes this tree. The vocabulary is data the distribution ships and answers in any clone; the closure is read from the local half of the layer, which this process serves to the worker in front of it and never publishes. Read, never written: `majordomus evidence` records, and a second writer for one ledger would be a second account of the same events.

GET /api/v1/obligations obligations.vocabulary

Every obligation there is

The tokens a task may declare in `requires`: what each one asks of a worker, the command that discharges it, the pathspecs its evidence is hashed over, and whether its fact is remote and therefore bound to a commit rather than to a tree. Shipped data, identical in every clone, so this answers in a checkout that has never run the lifecycle.

  • query
  • behaviorally_verified
  • benchmark required
  • cache process (max_entries=2)
  • MCP tool majordomus_obligations
  • MCP resource {"name": "obligations", "uri": "majordomus://obligations"}

No parameters.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/obligations"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 Vocabulary Every obligation there is: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/obligations/closure obligations.closure

What this task still owes

Every obligation the active task declared, joined with what the vocabulary says about it and with the evidence that does or does not discharge it: what is owed, what is discharged, and what has gone stale — with the recorded input hash and the tree's current one, or the recorded commit and its label, so a reader can see against what. The judgement is the one `finish` applies, reproduced rather than re-decided, and the staleness words are the repository's only four. A checkout with no task reports that, rather than reporting nothing owed.

  • query
  • behaviorally_verified
  • benchmark required
  • cache process (max_entries=2, ttl_seconds=2)
  • MCP tool majordomus_obligation_closure
  • MCP resource {"name": "obligation-closure", "uri": "majordomus://obligations/closure"}

No parameters.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/obligations/closure"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 Closure What this task still owes: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.

Schemas

The components only this tag's operations use. Each row is the type's own field, its doc comment the description; a type another page owns links there.

Closure

object
What this checkout's active task owes, and how much of it is still true.

A clone that has never run the lifecycle answers this too, and answers it as absence: present false, no task, and a finding saying so. "Nothing owed" and "nothing to owe it" are different facts, and a served instance must not report the second as the first.

use majordomus_cli::capability::builtin::obligations::Closure;
let fresh: Closure = serde_json::from_str(
    r#"{"present":false,"worktree":"/srv/clone","branch":"master","closed":false,
        "tallies":{},
        "findings":["no active task in this checkout (.ai/local/state/current.yaml); nothing owes anything here"]}"#,
)
.unwrap();
assert!(!fresh.present && !fresh.closed, "nothing that does not exist is closed");
assert!(fresh.task.is_none() && fresh.obligations.is_empty());
assert_eq!(fresh.findings.len(), 1, "absence is reported, not implied");
propertytyperequireddescription
present boolean yes Whether this checkout has a task to report about at all. False in a clone that has never run the lifecycle, which is not a fault and is not "nothing owed".
worktree string yes The worktree this answer is about. Every reading below is scoped to it, and to no other checkout of the same repository.
branch string yes The branch, or DETACHED.
head string no The commit this checkout is on.
working_tree string no clean or dirty.
task ActiveTask | null → ActiveTask no The active task, or None. Its requires is the list the entries below expand.
closed boolean yes True when the task declares obligations and every one of them is discharged and current — that is, when majordomus.obligation-closure would not refuse finish --outcome completed. False when anything is owed, stale or undeclared, and false when there is no task: nothing that does not exist is closed.
tallies map of integer (uint) yes How many obligations stand where, by state word. Absent states are absent rather than zero, so a reader never has to know the vocabulary to read the tallies.
obligations array of ObligationClosure → ObligationClosure no Every obligation the task declared, in the order it declared them.
findings array of string no What a reader should know before trusting any of the above: an unreadable vocabulary, a ledger that could not be read, a task that declares nothing. Empty is the ordinary case.

Used by obligations.closure

Evidence

object
The task.evidence line that discharged an obligation, as the ledger holds it.

Nothing older is consulted: evidence is superseded by evidence, and the ledger keeps the history for a reader that wants it.

use majordomus_cli::capability::builtin::obligations::Evidence;
let e: Evidence = serde_json::from_str(
    r#"{"recorded_at":"2026-09-09T21:01:00Z","head":"f00ba7","branch":"master",
        "kind":"ci","command":"scripts/ci/reference-check","inputs_hash":"7f68b9"}"#,
)
.unwrap();
assert_eq!(e.head, "f00ba7", "the envelope's commit is what a remote fact is judged against");
assert!(e.artifact.is_empty(), "one of command and artifact; narrative is not evidence");
propertytyperequireddescription
recorded_at string yes When the line was appended.
head string yes The commit the ledger's envelope stamped on it.
branch string no The branch it was recorded on.
kind string no How it was taken: test, build, ci, artifact or manual.
command string no The command that produced it. Narrative is not evidence, so one of this and artifact is always present.
artifact string no The reference it points at, such as a published URL.
result string no What the command said, when its output was the point.
session string no The episode that recorded it, when one was open.
inputs_hash string no The hash of the token's declared inputs at the moment it was taken. Empty for a token that declares none.

Used by ObligationClosure

Obligation

object
One token a task may declare in requires, as the distribution ships it.

remote and inputs are what decide how its evidence is judged, and they are the file's decision rather than this reader's: a fact the working tree cannot establish is bound to the commit it was taken at, and everything else is bound to the bytes of the files its pathspecs select.

use majordomus_cli::capability::builtin::obligations::Obligation;
let push: Obligation = serde_json::from_str(
    r#"{"id":"push","title":"The commit reached the remote",
        "summary":"The branch's head exists on the remote it tracks.",
        "discharged_by":"git","remote":true}"#,
)
.unwrap();
assert!(push.remote);
assert!(push.inputs.is_empty(), "a remote fact is bound to a commit, not to a tree");
propertytyperequireddescription
id string yes The token, as a task's requires names it.
title string yes The obligation as a heading.
summary string yes What a worker is being asked to have done.
discharged_by string yes The command that produces the evidence. none for a token held by another line of the contract and listed so that a report can say it rather than leave a hole.
inputs array of string no The pathspecs the evidence is hashed over. Empty for a token whose fact is remote, and empty for one bound to neither: see ObligationClosure::staleness.
remote boolean yes True when the fact cannot be established from the working tree alone — a push, a publication, a deployment. Such evidence is bound to a commit, not to a tree.
note string no What the vocabulary says about the token beyond its summary.

Used by Vocabulary

ObligationClosure

object
One obligation the task declared, joined with what the vocabulary says about it and with the evidence that does or does not discharge it.

The vocabulary's fields are repeated here on purpose: a client asking what this task owes gets the token's title, its summary and the command that would discharge it in the same answer, and needs no second call to render a report.

use majordomus_cli::capability::builtin::obligations::{ObligationClosure, ObligationState};
let owed: ObligationClosure = serde_json::from_str(
    r#"{"id":"pages","title":"The published site serves this commit","remote":true,
        "state":"owed","detail":"owed, and no evidence was recorded",
        "reproduce":"majordomus evidence --covers pages --command 'scripts/pages verify'"}"#,
)
.unwrap();
assert_eq!(owed.state, ObligationState::Owed);
assert!(owed.evidence.is_none() && owed.staleness.is_none(), "nothing to label");
assert!(owed.reproduce.starts_with("majordomus evidence"), "a finding carries its repair");
propertytyperequireddescription
id string yes The token, as the task's requires names it.
title string yes From the vocabulary; the token itself when it declares none.
summary string no From the vocabulary.
discharged_by string no From the vocabulary: the command that produces the evidence.
remote boolean yes From the vocabulary: whether the fact is remote.
inputs array of string no From the vocabulary: the pathspecs the evidence is hashed over.
state ObligationState → ObligationState yes Where it stands.
staleness Divergence | null → Divergence no How far the evidence is from this checkout, in the repository's one staleness vocabulary. None when there is no evidence to label.

A remote fact is labelled against the commit it was taken at, because the site that serves a commit goes on serving it while the tree moves underneath. A tree-bound fact is labelled by re-hashing: equal hashes are exact, and a difference takes the commit's label, which is advanced when the working tree has merely moved on and diverged when the history it named is gone. A token that declares neither inputs nor a remote fact is bound to nothing and stays exact once recorded — that is the validator's behaviour, and it is reproduced rather than tightened here.

evidence Evidence | null → Evidence no The line that discharged it, or None.
inputs_hash_now string | null no What the token's inputs hash to in this tree now — the other half of "stale against what". None for a token that declares no inputs, and None when git could not be asked which files they select.
inputs_files integer | null no How many tracked files that hash was taken over. Reported because the shell's selection and this one are not always the same set — see the module header — and a count is the cheapest way for a reader to see it rather than be told it.
detail string yes One line: what this obligation's standing actually is, in the words the validator uses when it refuses.
reproduce string yes The command that would discharge it. Every finding here carries the way to act on it, as project.finding-carries-reproduce asks.

Used by Closure

ObligationState

"owed" | "discharged" | "stale" | "undeclared"
Where one obligation stands.

The words are the validator's verdicts, not a severity scale: stale is not a worse owed, it is evidence that was true and no longer describes what it proved.

use majordomus_cli::capability::builtin::obligations::ObligationState;
// only one of the four lets a task be called completed
let completable = |s: ObligationState| s == ObligationState::Discharged;
assert!(completable(ObligationState::Discharged));
assert!(!completable(ObligationState::Stale), "evidence that no longer describes the tree");
assert!(!completable(ObligationState::Owed));
assert!(!completable(ObligationState::Undeclared));
  • owedDeclared, and no evidence for it was ever recorded.
  • dischargedEvidence exists and still describes this tree, or this commit.
  • staleEvidence exists and no longer describes what it proved: the inputs changed, or the commit it named is not this one.
  • undeclaredThe task requires a token the shipped vocabulary does not declare. Nothing can discharge it, because nothing knows what would.

Used by ObligationClosure

Vocabulary

object
The vocabulary the distribution ships: every token there is.

count is measured from obligations rather than written down, so a token added to the file is counted by the same act that declares it.

use majordomus_cli::capability::builtin::obligations::Vocabulary;
let v: Vocabulary = serde_json::from_str(
    r#"{"version":1,"source":"/opt/majordomus/share/obligations.yaml","count":1,
        "obligations":[{"id":"commit","title":"The work is committed",
                        "summary":"In the branch's history, not the working tree.",
                        "discharged_by":"git","remote":false}]}"#,
)
.unwrap();
assert_eq!(v.count, v.obligations.len());
assert_eq!(v.version, 1, "the only shape this reader accepts");
propertytyperequireddescription
version integer (uint64) yes The file's own format version. 1 is the only one this reader accepts.
source string yes Where it was read from, absolute: the distribution's share directory, which is not necessarily inside the repository.
count integer (uint) yes How many tokens there are. No number anywhere is written down.
obligations array of Obligation → Obligation yes Every token, in the order the file declares them.

Used by obligations.vocabulary

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