plan
The milestone and issue model of this repository, and everything derived from it that nobody authored: the status of each record, the dependency graphs above and below the milestone boundary, the topological execution waves, the roadmap order, the milestone being executed and the one issue to take next. Status is never stored — a record says what happened to it and the status follows from that and from the state of its dependencies — so no file can contradict the graph. This capability reads; writing a lifecycle marker into a record is `plan.transition`, which declares that it writes the repository.
The whole derived plan
Every milestone and issue with its derived status, wave, rank, both directions of its graph and its counts; the execution waves; both dependency graphs as edges; every validation finding; and the plan's header with the active milestone derived. The one value every other capability of this module answers out of. Derived on every call: a transition writes a lifecycle marker into a record between two calls, and a plan answered from a snapshot would send two workers to one issue.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_plan
- MCP resource {"name": "plan", "uri": "majordomus://plan"}
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/plan"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
Plan |
The whole derived plan: 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. |
The issues, filtered by what the graph derived
One record per issue with its derived status, its wave, the dependencies it declares, the ones that are not DONE (plus `milestone:<id>` when the gate holds the whole outcome back), the issues that depend on it, the paths it touches and its evidence tally. Filtering by `status: READY` is the ready set and by `status: BLOCKED` the blocked set; nothing here is a separate derivation.
- query
- behaviorally_verified
- benchmark required
- cache generation (max_entries=16)
- MCP tool majordomus_plan_issues
Query parameters
| name | type | required | description | examples |
| milestone |
string |
no |
Only issues of this milestone. Default: every issue of the plan. |
one-milestone: "M000" |
| status |
string |
no |
Only issues in this derived status — `READY` for the ready set, `BLOCKED` for the
blocked set. The vocabulary travels with every answer, so a caller never has to know
which statuses exist. |
ready: "READY" |
| wave |
integer (uint32) |
no |
Only issues in this execution wave. |
|
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/plan/issues?milestone=M000&status=READY"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanIssueList |
The issues, filtered by what the graph derived: 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. |
The one issue to take now
The lowest-wave READY issue of the active milestone, highest priority first, then id. The active milestone can have nothing ready while another one does — one waiting on its own acceptance evidence, for instance — so the search widens to the whole plan rather than answering `none` and sending a worker away from work that is genuinely executable. This is what an agent asks before it starts.
- query
- behaviorally_verified
- benchmark required
- cache generation (max_entries=16)
- MCP tool majordomus_plan_next
Query parameters
| name | type | required | description | examples |
| milestone |
string |
no |
Restrict the answer to one milestone. Default: the whole plan, and for `next` the
active milestone with the rest of the plan as the fallback. |
one-milestone: "M000" |
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/plan/next?milestone=M000"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanNextIssue |
The one issue to take now: 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. |
One milestone or issue, with everything derived about it
A milestone with its issues in full, or an issue with the issues it waits on in full. The record's own prose stays where it has always been — `majordomus://issue/<id>` returns the file — and this answers what the file cannot say about itself: what its status is, where it sits in the graph, and what is between it and being executable.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_plan_record
Query parameters
| name | type | required | description | examples |
| id |
string |
yes |
The id of a milestone or an issue, as its file is named (`I0001`, `work-graph-github`). |
issue: "I0001"milestone: "M000" |
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/plan/record?id=I0001"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanRecord |
One milestone or issue, with everything derived about it: 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. |
The milestones in derived order
The milestone graph laid out by rank, with `order` breaking ties inside a rank only, and the first unblocked unfinished milestone as `now` and the one after it as `next`. Nothing in the sequence is authored: a milestone whose prerequisites are not real cannot be nominated, which is what makes `each step is gated by the previous one being real` an invariant rather than a sentence.
- query
- behaviorally_verified
- benchmark required
- cache generation (max_entries=4)
- MCP tool majordomus_plan_roadmap
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/plan/roadmap"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanRoadmap |
The milestones in derived order: 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. |
Where the plan stands
Every milestone with its derived status and its issues counted by status, the milestone a worker is executing now, the next ready issue in full, and the plan's own totals. The counts are keyed by the declared vocabulary, which travels with the answer, so a status added to the engine appears here without anything being edited.
- query
- behaviorally_verified
- benchmark required
- cache generation (max_entries=16)
- MCP tool majordomus_plan_status
Query parameters
| name | type | required | description | examples |
| milestone |
string |
no |
Restrict the answer to one milestone. Default: the whole plan, and for `next` the
active milestone with the rest of the plan as the fallback. |
one-milestone: "M000" |
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/plan/status?milestone=M000"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanStatusReport |
Where the plan stands: 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. |
Move one issue through the lifecycle
Record that execution of an issue began (`start`), that implementation is complete with evidence outstanding (`verify`), or that it is finished (`done`). One field of the issue's own record is stamped, the seal of its event is written beside it, and the event is appended to the ledger; a stamp without that seal moves no status. The move is refused when the model says it is illegal — an issue that is not READY cannot start, one that was never ACTIVE cannot be verified, and one that was never started, whose dependencies are unfinished or whose required evidence is absent cannot be done — and the refusal names what is in the way. The status that comes back is derived from the record afterwards, not announced by the move: a `done` whose evidence is missing leaves the issue in VERIFY and says so.
- command
- behaviorally_verified
- benchmark required_when
- cache disabled
- MCP tool majordomus_plan_transition
example refused-by-status
{"issue":"I0001","transition":"start"}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 -X POST "http://127.0.0.1:8741/api/v1/plan/transition" -H "content-type: application/json" -d '{"issue":"I0001","transition":"start"}'Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanTransitionResult |
Move one issue through the lifecycle: 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. |
| 422 |
ErrorBody |
refused: The command was understood and turned down for the reason the message gives. |
| 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. |
What the model refuses
Every finding the derivation produced, in the order it produced them: a dependency on something that is not an issue, a cycle, an issue executing ahead of its dependencies or of its milestone's gate, an issue with no acceptance criteria, evidence missing under a completion date, a lifecycle stamp its transition did not write (no seal of its plan_* event beside it), a milestone whose graph contradicts itself, two issues of one wave sharing a path. A failure means the model is invalid; a warning means it is legal and worth reading.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_plan_validate
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/plan/validate"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanValidation |
What the model refuses: 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. |
What may run at the same time
The topological layering of the issue graph: an issue enters a wave only once every dependency has left it, so its wave is one past the longest path to it. Sharing a wave is a necessary condition for running two issues at once, not a sufficient one — overlapping scope serialises them, and every such overlap is reported beside the waves rather than left for two workers to discover in a merge conflict.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_plan_waves
Query parameters
| name | type | required | description | examples |
| milestone |
string |
no |
Restrict the answer to one milestone. Default: the whole plan, and for `next` the
active milestone with the rest of the plan as the fallback. |
one-milestone: "M000" |
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/plan/waves?milestone=M000"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
PlanWaveReport |
What may run at the same time: 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.
Plan
object
The whole derived plan: what every plan capability answers out of.
| property | type | required | description |
| project |
PlanProject → PlanProject |
yes |
The plan's header, with the active milestone derived. |
| statuses |
PlanVocabulary → PlanVocabulary |
yes |
The declared status vocabularies. |
| milestones |
array of PlanMilestone → PlanMilestone |
yes |
Every milestone, in id order. |
| issues |
array of PlanIssue → PlanIssue |
yes |
Every issue, in id order. |
| waves |
array of PlanWave → PlanWave |
yes |
The execution waves, lowest first. |
| edges |
array of PlanEdge → PlanEdge |
yes |
The issue dependency graph, sorted. |
| milestone_edges |
array of PlanEdge → PlanEdge |
yes |
The milestone dependency graph, sorted. |
| findings |
array of PlanFinding → PlanFinding |
yes |
Every finding, in derivation order. |
Used by plan.model
PlanCounts
object
A milestone's issues counted: the two denominators, then one entry per declared status.
| property | type | required | description |
| total |
integer (uint32) |
yes |
Every issue naming this milestone. |
| required |
integer (uint32) |
yes |
The denominator the milestone's own status derivation uses: total less cancelled, so
no surface prints "n of total" for a milestone the engine calls DONE. |
| by_status |
map of integer (uint32) |
yes |
One entry per declared issue status, keyed by the vocabulary. |
Used by PlanMilestone PlanMilestoneProgress PlanStatusReport
PlanEdge
object
One dependency edge, from before to.
| property | type | required | description |
| from |
string |
yes |
The record that must be real first. |
| to |
string |
yes |
The record that waits for it. |
Used by Plan
PlanFinding
object
One validation finding, in the shape project.finding-carries-reproduce asks for.
| property | type | required | description |
| level |
string |
yes |
FAIL or WARN. A failure means the model is invalid. |
| code |
string |
yes |
The stable code a reader greps for (unknown_dependency, scope_conflict, …). |
| subject |
string |
yes |
The record the finding is about, or graph when it is about the whole graph. |
| message |
string |
yes |
What is wrong, in one line. |
Used by Plan PlanValidation PlanWaveReport
PlanIssue
object
One issue, as its record declares it and as the graph derives it.
| property | type | required | description |
| id |
string |
yes |
The identity, which is also the file name. |
| milestone |
string |
yes |
The milestone it belongs to. |
| status |
string |
yes |
The derived status. Never stored: an issue records what happened to it and the
status follows from that and from the state of its dependencies. |
| wave |
integer (uint32) |
yes |
The execution wave: one past the longest path to it through the dependency graph. |
| priority |
string |
yes |
p0 … p3. |
| profile |
string |
yes |
The execution profile the issue is worked under. |
| parallel_safe |
boolean |
yes |
Whether it may run beside another issue of its wave. |
| title |
string |
yes |
One line naming the outcome. |
| slug |
string |
yes |
The slug, when the record carries one. |
| depends_on |
array of string |
yes |
Every issue it declares a dependency on, as declared — including one that does not
exist, which is a finding rather than a silent omission. |
| blocked_by |
array of string |
yes |
The dependencies that are not DONE, plus milestone:<id> when the milestone gate
holds the whole outcome back. |
| dependents |
array of string |
yes |
The issues that depend on this one. |
| scope |
array of string |
yes |
The paths it touches; two issues of one wave that share a path are serialised. |
| serves |
array of string |
yes |
The intent criteria it exists to make true, each <intent>#<criterion>, as declared.
Empty for maintenance work under a milestone no intent names; whether a named
criterion exists is the intent coverage's question, not the plan's. |
| objective |
string |
yes |
One line: what the issue is for. |
| evidence_have |
integer (uint32) |
yes |
Evidence entries attached. |
| evidence_need |
integer (uint32) |
yes |
Evidence tokens the record requires before it may be DONE. |
| started_at |
string |
yes |
When execution began, when it did. |
| verified_at |
string |
yes |
When implementation was declared complete. |
| completed_at |
string |
yes |
When completion was recorded. |
Used by Plan PlanIssueList PlanNextIssue PlanRecord PlanStatusReport PlanWaveView
PlanIssueList
object
Issues matching a filter, with the vocabulary that names their statuses.
| property | type | required | description |
| issues |
array of PlanIssue → PlanIssue |
yes |
The matching issues, in id order. |
| total |
integer (uint) |
yes |
How many matched. |
| statuses |
PlanVocabulary → PlanVocabulary |
yes |
The declared status vocabularies. |
Used by plan.issues
PlanMilestone
object
One milestone, as its record declares it and as the two graphs derive it.
| property | type | required | description |
| id |
string |
yes |
The identity, which is also the file name; a stable slug, never a version. |
| status |
string |
yes |
The derived status. |
| order |
integer (int64) |
yes |
Tie-break inside one rank; the roadmap is ordered by rank first. |
| priority |
string |
yes |
p0 … p3. |
| title |
string |
yes |
One line naming the outcome. |
| slug |
string |
yes |
The slug, when the record carries one. |
| version |
string |
yes |
The release the milestone belongs to, when it declares one. |
| rank |
integer (uint32) |
yes |
Its layer in the milestone graph: what orders the roadmap. |
| depends_on |
array of string |
yes |
The milestones it requires. |
| blocked_by |
array of string |
yes |
Those of them that are not DONE. Non-empty means the gate holds every issue of this
milestone back, whatever the issue graph says. |
| dependents |
array of string |
yes |
The milestones that require it. |
| claims |
array of string |
yes |
The claims of the repository this outcome makes true. |
| counts |
PlanCounts → PlanCounts |
yes |
Its issues, counted by derived status. |
| issues |
array of string |
yes |
Its issues, in id order. |
| outcome |
string |
yes |
One line: what is true once the milestone is real. |
Used by Plan PlanRecord PlanRoadmap
PlanMilestoneProgress
object
One milestone's progress, without its prose.
| property | type | required | description |
| id |
string |
yes |
The identity. |
| status |
string |
yes |
The derived status. |
| title |
string |
yes |
One line naming the outcome. |
| counts |
PlanCounts → PlanCounts |
yes |
Its issues, counted by derived status. |
Used by PlanStatusReport
PlanNextIssue
object
The one issue a worker should take now.
| property | type | required | description |
| issue |
PlanIssue | null → PlanIssue |
no |
The issue, when the plan has one that is executable. |
| active_milestone |
string |
yes |
The milestone the search started in. |
| reason |
string | null |
no |
Why there is none, when there is none: what to run to see what is in the way. |
Used by plan.next
PlanProject
object
The plan's header: what this repository is, plus the one field nobody authors.
| property | type | required | description |
| name |
string |
yes |
The project's name, from project.yaml. |
| repository |
string |
yes |
The repository it belongs to, owner/name. |
| default_branch |
string |
yes |
The branch the plan is executed on. |
| active_milestone |
string |
yes |
The milestone a worker is executing now: the lowest-ranked unblocked milestone that
is ACTIVE, else the lowest-ranked unblocked one not finished. Derived on every read,
stored nowhere, and never authored — the plan cannot nominate a milestone whose
prerequisites are not real. |
Used by Plan PlanStatusReport
PlanRecord
object | object
One record of the plan, milestone or issue, with everything derived about it.
Used by plan.record
PlanRoadmap
object
The milestones in derived order, with the one being executed and the one after it.
| property | type | required | description |
| milestones |
array of PlanMilestone → PlanMilestone |
yes |
The milestones ordered by rank, then order, then id. The sequence is derived from
the milestone graph; no list of versions is maintained anywhere. |
| now |
string | null |
no |
The first unfinished, unblocked milestone in that sequence. |
| next |
string | null |
no |
The one after it, blocked or not. |
Used by plan.roadmap
PlanStatusReport
object
Where the plan stands: every milestone's progress, the milestone being executed, and the
one issue to take next.
| property | type | required | description |
| project |
PlanProject → PlanProject |
yes |
The plan's header, with the active milestone derived. |
| statuses |
PlanVocabulary → PlanVocabulary |
yes |
The declared status vocabularies. |
| milestones |
array of PlanMilestoneProgress → PlanMilestoneProgress |
yes |
Every milestone, in id order. |
| next_ready |
PlanIssue | null → PlanIssue |
no |
The next ready issue, when there is one. |
| counts |
PlanCounts → PlanCounts |
yes |
Every issue of the plan, counted by derived status. |
Used by plan.status
PlanTransition
"start" | "verify" | "done"
One lifecycle move.
Three, not four: evidence appends a record rather than moving an issue, and it belongs
with the record it proves rather than with the moves it unblocks.
use majordomus_cli::plan::Transition;
// The wire spelling is the one a caller types on the command line.
let t: Transition = serde_json::from_str("\"done\"").unwrap();
assert_eq!(t, Transition::Done);
assert_eq!(serde_json::to_string(&Transition::Start).unwrap(), "\"start\"");
// Each move owns the field it stamps and the event it records; nothing else decides
// either, so a new move cannot be added without saying both.
for (t, field, event) in [
(Transition::Start, "started_at", "plan_start"),
(Transition::Verify, "verified_at", "plan_verify"),
(Transition::Done, "completed_at", "plan_done"),
] {
assert_eq!(t.field(), field);
assert_eq!(t.event(), event);
}
startExecution began. Refused unless the issue is READY.verifyImplementation is complete and the evidence is still outstanding.doneComplete. Refused while anything it waits on is unfinished, or while any evidence
its own record requires is absent.
Used by PlanTransitionInput PlanTransitionResult
PlanTransitionInput
object
The input of
plan.transition: which issue to move, and which way.
Two fields and no third. There is no timestamp — the move is stamped with the moment it
happened, never with one the caller supplies, because a lifecycle a caller can backdate
is not a record of what happened. There is no force or reason field either: a transition
the model refuses is refused, and the way to make it legal is to satisfy the guard rather
than to ask past it.
use majordomus_cli::capability::builtin::plan::PlanTransitionInput;
use majordomus_cli::plan::Transition;
// The wire form an MCP client or an HTTP POST sends.
let input: PlanTransitionInput =
serde_json::from_str(r#"{"issue":"I0001","transition":"done"}"#).unwrap();
assert_eq!(input.issue, "I0001");
assert_eq!(input.transition, Transition::Done);
assert_eq!(input.transition.field(), "completed_at");
assert_eq!(input.transition.event(), "plan_done");
// Unknown fields are refused rather than ignored: a caller that misspells `transition`
// is told so instead of having its move silently dropped.
assert!(serde_json::from_str::<PlanTransitionInput>(
r#"{"issue":"I0001","transition":"done","force":true}"#).is_err());
| property | type | required | description |
| issue |
string |
yes |
The issue to move, by its id — which is also its file name. |
| transition |
PlanTransition → PlanTransition |
yes |
Which move: start, verify or done. |
Used by plan.transition
PlanTransitionResult
object
What one transition did.
from and to are both derived statuses, read before and after the write. They are
reported rather than assumed because the move does not determine the status on its own:
done on a record whose evidence is incomplete leaves it in VERIFY, and the caller is
told that rather than told it succeeded.
use majordomus_cli::capability::builtin::plan::PlanTransitionResult;
// What a `done` looks like when the record's required evidence is not all present: the
// call succeeded, the field was written, and the issue is still in VERIFY. A caller that
// read only the absence of an error would report this as finished; a caller that reads
// `to` knows better, which is why the field is here.
let held: PlanTransitionResult = serde_json::from_str(r#"{
"issue":"I0001","transition":"done","from":"VERIFY","to":"VERIFY",
"field":"completed_at","at":"2026-09-11T12:00:00Z","event":"plan_done"
}"#).unwrap();
assert_eq!(held.to, "VERIFY", "the status is derived from the record, not from the move");
// `next_ready` is absent when the plan has none left to hand out, which is ordinary
// rather than an error, so it is omitted rather than spelled as an empty string.
assert!(held.next_ready.is_none());
| property | type | required | description |
| issue |
string |
yes |
The issue that moved. |
| transition |
PlanTransition → PlanTransition |
yes |
The move that was performed. |
| from |
string |
yes |
The status the issue was in before. |
| to |
string |
yes |
The status it is in now, derived from the record as written. |
| field |
string |
yes |
The field that was stamped. |
| at |
string |
yes |
The timestamp written into it, and into updated_at. |
| event |
string |
yes |
The ledger event appended. |
| next_ready |
string | null |
no |
The issue a worker should take next, derived after the move. Absent when the plan
has none — which after a start is the ordinary case, because the issue just taken
is no longer READY. |
Used by plan.transition
PlanValidation
object
The model's own validation: what the graph refuses and what it merely warns about.
| property | type | required | description |
| valid |
boolean |
yes |
Whether the model is valid: no finding is a failure. |
| milestones |
integer (uint) |
yes |
How many milestones the plan holds. |
| issues |
integer (uint) |
yes |
How many issues. |
| failures |
integer (uint) |
yes |
How many findings are failures. |
| warnings |
integer (uint) |
yes |
How many are warnings. |
| findings |
array of PlanFinding → PlanFinding |
yes |
Every finding, in derivation order. |
Used by plan.validate
PlanVocabulary
object
The status vocabularies, so a reader never has to know which statuses exist.
| property | type | required | description |
| issue |
array of string |
yes |
The issue statuses, in derivation order. |
| milestone |
array of string |
yes |
The milestone statuses, in derivation order. |
Used by Plan PlanIssueList PlanStatusReport
PlanWave
object
One execution wave: the issues the graph allows to run at the same time.
| property | type | required | description |
| wave |
integer (uint32) |
yes |
The layer, from zero. |
| issues |
array of string |
yes |
The issues in it, in id order. |
Used by Plan
PlanWaveReport
object
The execution waves, with the overlaps that serialise issues the graph would let run
together.
| property | type | required | description |
| waves |
array of PlanWaveView → PlanWaveView |
yes |
The waves, lowest first, with the issues of each. |
| serialised_by_scope |
array of PlanFinding → PlanFinding |
yes |
Scope overlaps between two issues of one wave. Two issues sharing a wave is a
necessary condition for running them at once, not a sufficient one: overlapping
scope serialises them, and the overlap is reported here rather than left for two
workers to discover in a conflict. |
Used by plan.waves
PlanWaveView
object
One wave, with the issues in it.
| property | type | required | description |
| wave |
integer (uint32) |
yes |
The layer, from zero. |
| issues |
array of PlanIssue → PlanIssue |
yes |
The issues in it, in id order. |
Used by PlanWaveReport