This document explains what the model means. It contains no current figures: what is true
right now is printed by majordomus plan status and rendered on the website's roadmap from
the same data. If you want to know how many issues are ready, run the command.
majordomus start supervises one task. This layer supervises the thing a task belongs to.
The problem
A task record says what one session is doing. It does not say why that work exists, what it depends on, what would prove it finished, or what should be done after it. That knowledge lived in conversation, and conversation does not survive a session, a provider switch, or a handover to a person.
Three failures follow. Work gets picked from memory rather than from a dependency order, so something is started before the thing it needs. "Done" gets asserted rather than shown. And the plan exists in several places at once — a chat, a README paragraph, a hand-drawn diagram, a GitHub milestone — which disagree the first time one of them changes.
The two records
A milestone is an executable specification of an outcome. Not a folder for tickets: a
statement of the problem, the outcome that ends it, the current and desired state, what is in
scope and what is deliberately not, the criteria that would make it true, the validation that
would demonstrate it, and the evidence required to accept it. One file under
.ai/repo/project/milestones/.
An issue is a bounded execution contract. It carries enough for a worker with no
conversation history to execute it: the objective, why it exists, the current and desired
state, the paths it may touch, what it depends on, the acceptance criteria, the validation
commands, and the evidence its completion requires. One file under
.ai/repo/project/issues/.
Both are hand-written YAML in the same restricted subset the policy uses, and both are checked against an allowlist: a key nobody reads is an error, not a comment.
An issue names its milestone. A milestone does not list its issues. One direction means the two can never disagree about which issues belong to which outcome.
Status is derived
Neither record has a status field. Writing one is an unknown key and fails validation.
An issue records what happened to it — started_at, verified_at, completed_at,
cancelled, and its evidence — and the status follows:
| status | when |
|---|---|
CANCELLED | cancelled: true |
DONE | completed_at is set and every evidence_required token has evidence |
VERIFY | implementation is claimed complete and the evidence is not yet sufficient |
ACTIVE | started_at is set and nothing further is recorded |
BLOCKED | a dependency is not DONE |
READY | nothing above applies |
A milestone's status follows from its issues and its own evidence:
| status | when |
|---|---|
PLANNED | it has no issues, or none of them has moved |
ACTIVE | at least one issue is active, verifying or done |
BLOCKED | it is unfinished and no issue is ready |
VERIFY | every issue that is not cancelled is done and the milestone's own evidence is not complete |
DONE | that evidence is complete too |
A milestone is never DONE because a count of closed issues reached its total. The last step
is the milestone's own acceptance, and it is evidence-gated like every other step.
The active milestone is derived the same way: the lowest-ordered milestone that is
ACTIVE, or failing that the lowest-ordered one that is not finished. Nothing declares it.
The graph
depends_on is a list of issue ids. The graph they form is validated, not trusted. Each of
these is a distinct finding naming the issue that caused it:
- a dependency on an issue that does not exist
- an issue that depends on itself
- the same dependency named twice
- an issue naming a milestone that does not exist
- a cycle, with every issue trapped in or behind it named
- an issue that is
ACTIVE,VERIFYorDONEwhile a dependency is notDONE - an issue with no acceptance criteria or no validation command — a placeholder
A failing finding makes majordomus plan validate and majordomus doctor exit non-zero.
majordomus watch reports the same violations as drift. Work in progress is reported and
never blocked: an issue that is merely unfinished is not a failure of anything.
Execution waves
A wave is a layer of the graph. Wave zero is every issue with no dependencies; an issue sits one layer past its deepest dependency. Waves are computed on every read and stored nowhere, so an execution plan cannot go stale — changing one edge moves every wave that depends on it, immediately, on every surface.
Sharing a wave is necessary for two issues to run concurrently. It is not sufficient. If
their declared scope paths overlap — one equal to, inside, or containing another — the
overlap is reported and they serialise. The direction is deliberate: a false serialisation
costs time, a false parallel costs a conflict discovered after the work is done.
Evidence
An issue declares evidence_required as a list of tokens. majordomus plan evidence
attaches one piece of evidence against one token and refuses a token the issue does not
declare. It also refuses without a command or an artifact — narrative is not evidence.
majordomus plan done refuses while any token is uncovered. An issue whose completed_at
is set but whose evidence is incomplete derives VERIFY, never DONE. The evidence lives in
the issue's own file, beside the contract it satisfies, with the commit it was recorded at.
What this does not do: rerun the command. The tool records what a worker says a command produced. The commit hash stored beside it is what makes a false record checkable later.
Traceability: what realised an issue, and what an issue realised
The model reaches as far as a branch on its own: a branch path component equal to an issue id
(feature/I1305-traceability → I1305) is the one edge the topology already reads and the
pre-commit guard already enforces. Everything above it — the commits, the pull requests — is
derived on every read and stored nowhere. No canonical record under
.ai/repo/project/ names a branch, a commit, a pull request or a check run, and none may:
git and GitHub already hold those facts, and a record repeating one is a second truth that
starts rotting the moment history is rewritten.
Two systems hold the two halves, and the boundary between them is the same boundary
scripts/github-sync respects — the executable, bin/, lib/, share/ and test/ make no
network call, and test/cases/08_no_forbidden_constructs.sh proves it:
| half | source | where it lives |
|---|---|---|
| branches and commits | git for-each-ref, git log | the trace capability module of the Rust executable |
| pull requests | the GitHub API, through gh | scripts/traceability |
issue ──names──▶ branch ──contains──▶ commit derived from git
▲ ▲
│ └── head branch of ─── pull request derived from GitHub
└── milestone, the one edge git does not hold: the canonical issue record declares it
A branch's commits are the commits it holds that the trunk did not: measured against the trunk while the branch is open, and against the first parent of the merge commit that brought it in once it is merged. Read backwards, a commit belongs to the issue whose branches hold it.
Three answers are states rather than failures, and each is reported by name rather than silently dropped:
- absorbed — the branch reached the trunk with no merge commit of its own (fast-forwarded, or rebased onto it). Its commits cannot be told from the trunk's, so none are claimed and the trace says it is incomplete.
- unattributed — no branch naming an issue holds the commit, and no pull request's head branch names one. That is either work committed with no execution contract or a branch deleted after its merge, and the answer says so rather than choosing between them. Work with no contract is the thing a traceability report exists to make visible; omitting it would defeat the report.
- ambiguous — branches naming two different issues hold the same commit. The branch-name edge cannot decide, so it does not.
scripts/traceability every issue, and the trunk commits nothing accounts for
scripts/traceability --issue I1305 its branches, its commits and its pull requests
scripts/traceability --commit <rev> the issue and milestone it served, or unattributed
scripts/traceability --pull 42 the same, from the pull request's side
scripts/traceability --no-github the git half alone: no token, no network
scripts/traceability --strict exit 10 when something has no contract; the shape of a gate
The git half is majordomus_trace_issue, majordomus_trace_commit and
majordomus_traceability on MCP and /api/v1/trace, /api/v1/trace/issue,
/api/v1/trace/commit on HTTP; the script is a client of them rather than a second
implementation. MJ_GH_FIXTURE_PULLS reads the pull requests from a file in the shape the
live read produces, so the join is provable with no network and no token — the same seam
scripts/github-sync uses, and what test/cases/98_traceability.sh exercises.
Projections
The canonical files are the only source. Everything else is generated from them by one
engine — lib/project.awk, loaded by lib/project.sh — so no two surfaces can hold
different opinions about what is ready:
| surface | how |
|---|---|
| the command line | majordomus plan |
| the Mermaid DAG | majordomus plan graph |
| GitHub milestones and issues | scripts/github-sync, proved current by scripts/ci/github-check |
| the website's roadmap, milestone, issue and DAG pages | scripts/generate-site-data |
| the documentation | this file explains the semantics; the figures are generated |
GitHub is a projection and a place to talk, never the source. A canonical change updates the generated region of an issue body; a person editing that region is reported as drift and not overwritten; a person's comments and any text outside the region are never touched. Nothing is read back: closing an issue on GitHub does not complete it here.
A record is found on GitHub by an identity it carries in its own body, written beside the
hash as <!-- majordomus:record I0001 -->. A GitHub number is GitHub's to assign and a
title is a person's to edit, so neither can be the key: matching on a title means a rename
orphans the record and the next --apply creates a second one for work that already exists.
Every finding about a body is one of six states, from two independent questions — has a person rewritten the region since it was posted, and has the canonical record moved since?
| state | meaning | --apply |
|---|---|---|
insync | intact and current | nothing |
behind | intact, the canonical record has moved | rewrites |
edited | a person rewrote the region | refuses without --force |
conflict | a person rewrote it and the record has moved | refuses without --force |
adopt | no identity marker; matched by title, this once | writes one |
missing | no counterpart on GitHub | creates |
and, of a remote record rather than a canonical one, unmanaged: an issue claiming a
canonical id this repository does not have.
Applying is a deliberate act; agreement is a gate. scripts/ci/github-check reads the
remote on every change that can move either side, refuses the first six states outright,
and ratchets missing and adopt against .ai/repo/ci/github-drift-baseline.txt, which
may fall and may never rise. It exists because the detector was written, never called, and
the projection decayed to a tenth of the model over five days with every build green
(project.github-projection-gated@1).
The network calls live in scripts/github-sync, outside the tool. bin/, lib/, share/
and test/ contain no network client, and test/cases/08_no_forbidden_constructs.sh proves
it.
Working through the model
majordomus plan status # where the outcome stands
majordomus plan next # the one issue to take now
majordomus plan show I0007 # the whole contract, no chat history needed
majordomus plan start I0007 # refused unless it is READY
# ... execute, inside the scope the issue declares ...
majordomus plan verify I0007 # implementation complete, evidence pending
majordomus plan evidence I0007 --covers doctrine_test --type test \
--command "bash test/run.sh 44_model_doctrine" --result "1 passed"
majordomus plan done I0007 # refused while a required token is uncovered
majordomus plan next # recomputed, not chosen
A worker takes the issue the graph offers. Picking something else needs a reason: it is a blocker, it invalidates an assumption the milestone rests on, or a person reprioritised.
Replanning
The plan is expected to change. Add an issue, remove one, split one, merge two, move an edge, narrow a scope, change a validation — all of it is editing the canonical files, and all of it is re-validated on the next read. Two constraints survive every edit: the graph stays acyclic and resolvable, and evidence already recorded is not deleted to make a story tidier.
Replanning is not optional when an issue discovers a dependency nobody predicted, an acceptance criterion turns out to be untestable, an implementation path is disproven, or an issue becomes unnecessary. Leaving the graph describing a plan that reality has left behind is the failure this layer exists to prevent.
What was rejected
A status field with validation. Two sources of truth and a rule to reconcile them. The
graph is the only thing that can be right, so it is the only thing that is stored.
A milestone that lists its issues. A second edge, in the opposite direction, that can disagree with the first.
A separate evidence store. The contract and the proof that it was met belong in one record; splitting them lets one be read without the other.
A mapping file from canonical id to GitHub number. State that has to be kept in step with two systems. The id prefixes the title instead, and matching is a string comparison.
Estimates, velocity, burndown. None of them changes what a worker should do next, which is the only question this layer answers.
PLANNED as an issue status. It would only ever mean READY or BLOCKED with a
different word on it.
See also
docs/DOGFOODING.md— why this repository uses the model on itselfdocs/SCHEMAS.md— the fields of each filedocs/CLI.md—majordomus plan, subcommand by subcommanddocs/DOCTRINE.md— howmajordomus.project-integrityandmajordomus.dag-integrityare enforced