Skip to content

Planning

milestones as executable outcome specifications, issues as execution contracts, the dependency graph, derived status, execution waves, evidence, and the projections

Rendered from docs/PLANNING.md — the same Markdown GitHub shows.

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:

statuswhen
CANCELLEDcancelled: true
DONEcompleted_at is set and every evidence_required token has evidence
VERIFYimplementation is claimed complete and the evidence is not yet sufficient
ACTIVEstarted_at is set and nothing further is recorded
BLOCKEDa dependency is not DONE
READYnothing above applies

A milestone's status follows from its issues and its own evidence:

statuswhen
PLANNEDit has no issues, or none of them has moved
ACTIVEat least one issue is active, verifying or done
BLOCKEDit is unfinished and no issue is ready
VERIFYevery issue that is not cancelled is done and the milestone's own evidence is not complete
DONEthat 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, VERIFY or DONE while a dependency is not DONE
  • 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-traceabilityI1305) 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:

halfsourcewhere it lives
branches and commitsgit for-each-ref, git logthe trace capability module of the Rust executable
pull requeststhe GitHub API, through ghscripts/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:

surfacehow
the command linemajordomus plan
the Mermaid DAGmajordomus plan graph
GitHub milestones and issuesscripts/github-sync, proved current by scripts/ci/github-check
the website's roadmap, milestone, issue and DAG pagesscripts/generate-site-data
the documentationthis 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?

statemeaning--apply
insyncintact and currentnothing
behindintact, the canonical record has movedrewrites
editeda person rewrote the regionrefuses without --force
conflicta person rewrote it and the record has movedrefuses without --force
adoptno identity marker; matched by title, this oncewrites one
missingno counterpart on GitHubcreates

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

Claims this document defines

Each links to its own page with implementation, test and where it is used.