work-graph-github — The projection to GitHub is gated, identified and traversable, and a merged pull request leads back to the outcome it served
The projection cannot rot silently: a gate runs the drift check on every change that can move the model or the adapter, and drift is a CI failure with the record named. Identity travels with the record rather than being parsed out of its title, so renaming a GitHub title moves nothing and creates no duplicate, and the remote can be a fixture, so which record is which is provable with no network. The reconciliation vocabulary distinguishes the states it currently conflates, so `missing` no longer stands for both a record never created and a record a person deleted, and a remote edit is reported as a remote edit. Above the projection, a pull request names the issue it realises and the edge is derived from git and GitHub rather than remembered, so a merged commit leads back to the acceptance criteria it satisfied and an outcome leads forward to the evidence that it holds. The derivations the shell already computes — ready, blocked, waves, next, status — are readable through the same capability surface as every other thing this repository knows, rather than only from a terminal.
ACTIVE 10 of 12 issues done
- ready
- 2
- blocked
- 0
- active
- 0
- verify
- 0
- done
- 10
- cancelled
- 0
Problem
This repository already models work once: milestones are outcomes, issues are execution contracts, no status is stored, and `scripts/github-sync` projects both onto GitHub with a hash-marked region that protects what a human wrote. The model is not the defect. The defect is that nothing runs the detector: `core-check` runs `github-sync --plan` and `--render`, never `--check`, so the adapter's exit 11 has never fired in CI. The projection was applied once on 2026-09-04 and has not been applied since. Measured on 2026-09-09 at 867f3a9: 191 drift findings, of which 172 are canonical issues that have never existed on GitHub and 19 are bodies whose canonical text has moved on. All 8 milestones are in sync, which is exactly what makes the rot invisible: the surface a person looks at is the surface that still agrees. Underneath that, the adapter identifies a remote record by parsing its title for an `I0001 — ` prefix and reads the remote through a 200-item page that already holds 19 issues and 117 pull requests, so the mapping breaks when a person renames a title and the read truncates as soon as the backfill runs.
Outcome
The projection cannot rot silently: a gate runs the drift check on every change that can move the model or the adapter, and drift is a CI failure with the record named. Identity travels with the record rather than being parsed out of its title, so renaming a GitHub title moves nothing and creates no duplicate, and the remote can be a fixture, so which record is which is provable with no network. The reconciliation vocabulary distinguishes the states it currently conflates, so `missing` no longer stands for both a record never created and a record a person deleted, and a remote edit is reported as a remote edit. Above the projection, a pull request names the issue it realises and the edge is derived from git and GitHub rather than remembered, so a merged commit leads back to the acceptance criteria it satisfied and an outcome leads forward to the evidence that it holds. The derivations the shell already computes — ready, blocked, waves, next, status — are readable through the same capability surface as every other thing this repository knows, rather than only from a terminal.
Current state
The canonical model is 15 milestones and 184 issues, validated by `majordomus plan validate` (0 failures, 4 scope warnings) and enforced by `lib/doctor.sh` and `scripts/ci/core-check`. `scripts/github-sync` implements plan, check, apply and render with field ownership and a marker hash. GitHub holds 8 milestones and 19 issues. `--check` exits 11 on drift and no gate calls it. Issues and milestones are served as documents through `objects.get`, and none of the plan's derivations is a capability: `lib/plan.sh` computes them and the Rust executable reads `.ai/repo/project/issues/` for filenames only. Evidence carries an optional `commit` string and no pull request, check run or branch.
Desired state
A contributor moves an issue, runs the gates, and CI tells them the projection is behind before a reviewer does. A person renames an issue on GitHub and the next sync updates it rather than duplicating it. A reader asks which pull request realised an issue, or which outcome a merged commit served, and one command answers from derived edges. The plan's readiness is answerable over MCP and HTTP because it is a capability, not a shell function.
Scope
- scripts/github-sync
- scripts/ci
- .ai/repo/ci/gates.yaml
- .ai/repo/project
- .ai/repo/rules/project
- .ai/repo/adrs
- apps/majordomus-cli/src
- lib/plan.sh
- lib/project.sh
- docs
- site
- test/cases
Out of scope
- Replacing the canonical model, which is not the defect
- Importing GitHub semantics into canonical records; GitHub stays a projection
- Moving network calls into the tool itself, which test/cases/08_no_forbidden_constructs.sh forbids
- A GraphQL client, or any provider abstraction beyond the one GitHub host this repository publishes to
- Assignees, reactions, projects boards or discussions
Acceptance criteria
- A gate runs the drift check on every change that can move the model or the adapter, and a drift finding fails CI naming the record and the remedy
- Identity is canonical data: renaming a title on GitHub moves the mapping with it and creates no duplicate record
- Reconciliation is provable offline against a fixture remote, and a fixture is refused for any mutation
- The reconciliation vocabulary distinguishes never-created, deleted-remotely, edited-remotely, canonically-moved and conflicting, and each state names what would change before anything changes
- A pull request is related to the issue it realises by a derived edge, and the relation is traversable in both directions
- The plan's derivations are readable through the capability surface, and adding one requires no edit to a transport registry
- Every claim this milestone makes about GitHub is backed by a test that runs offline against a fixture
Validation
- scripts/github-sync --check
- scripts/ci/core-check
- bash test/run.sh
- cargo test --workspace --all-features
- majordomus generate --check
Evidence required
- drift_gated
- identity_stable
- offline_provable
- traceability_both_ways
Risks
- The backfill is an outward-facing mutation: applying the projection creates roughly 180 public issues in one run. It must be a deliberate, confirmed act, not a side effect of wiring a gate.
- Wiring the gate before the backfill makes CI red on master until the backfill lands. The order matters and is part of the work.
Evidence
A milestone is never done because its issues are; the last step is its own acceptance, and it is gated on the rows below.
| covers | type | command | result |
|---|---|---|---|
| drift_gated | test | bash test/run.sh 97_github_gate | tests: 1 passed, 0 failed, 0 skipped on master 01227a0f74a (2026-10-17); gate github-projection runs scripts/ci/github-check, and the github-projection workflow run 39004503235 was green on 01227a0f74a |
| identity_stable | test | bash test/run.sh 45_github_projection 602_a_spliced_body_is_recognised_as_its_own | tests: 2 passed, 0 failed, 0 skipped on master 01227a0f74a (2026-10-17): a rewritten remote title resolves by its marker, a spliced body reads back insync |
| offline_provable | test | bash test/run.sh 45_github_projection | tests: 1 passed, 0 failed, 0 skipped on master 01227a0f74a (2026-10-17): reconciliation runs against fixture remotes with no network and no token; --apply refuses a fixture |
| traceability_both_ways | test | bash test/run.sh 98_traceability 99_work_graph_chain 99_plan_capabilities | tests: 3 passed, 0 failed, 0 skipped on master 01227a0f74a plus the closed records (2026-10-17): an issue leads to its branch, merge and commits, and a commit leads back to its issue and milestone |
Issues
Each one is an execution contract with its own acceptance criteria, validation command and required evidence. The wave column is the layer of the dependency graph it sits in.
| issue | status | wave | depends on | title |
|---|---|---|---|---|
| I1401 | DONE | 0 | — | The identity of a projected record is what the record carries, not what its title says |
| I1402 | DONE | 1 | I1401 | A finding says which of six states it found, and the state that could hide a person's edit is no longer one of them |
| I1403 | DONE | 3 | I1401, I1402, I1404 | Backfill the projection once, deliberately, and record what it created |
| I1404 | DONE | 2 | I1402 | The drift check is a gate, so a projection that stops being applied cannot stay silent |
| I1405 | DONE | 1 | I1401 | A pull request names the issue it realises, and the edge is derived rather than remembered |
| I1406 | DONE | 0 | — | The plan's derivations are capabilities, so every surface reads the readiness the terminal reads |
| I1407 | DONE | 3 | I1404, I1405, I1406 | One issue travels the whole path and back, as an executable case |
| I1408 | DONE | 3 | I1404 | The documents stop claiming a projection nobody runs |
| I1516 | DONE | 0 | — | The adapter composes a projected body in one place |
| I2300 | DONE | 0 | — | A head is judged against what it changes, not against GitHub's live state |
| I2844 | READY | 0 | — | plan validate refuses a milestone whose GitHub title would be over 255 characters |
| I3920 | READY | 0 | — | The GitHub projection reads every issue, and a listing shorter than the repository refuses rather than reads as missing |
The graph
flowchart LR
I1401["I1401<br/>The identity of a projected record is what the record carries, not what its title says"]:::done
I1402["I1402<br/>A finding says which of six states it found, and the state that could hide a person's edit is no longer one of them"]:::done
I1403["I1403<br/>Backfill the projection once, deliberately, and record what it created"]:::done
I1404["I1404<br/>The drift check is a gate, so a projection that stops being applied cannot stay silent"]:::done
I1405["I1405<br/>A pull request names the issue it realises, and the edge is derived rather than remembered"]:::done
I1406["I1406<br/>The plan's derivations are capabilities, so every surface reads the readiness the terminal reads"]:::done
I1407["I1407<br/>One issue travels the whole path and back, as an executable case"]:::done
I1408["I1408<br/>The documents stop claiming a projection nobody runs"]:::done
I1516["I1516<br/>The adapter composes a projected body in one place"]:::done
I2300["I2300<br/>A head is judged against what it changes, not against GitHub's live state"]:::done
I2844["I2844<br/>plan validate refuses a milestone whose GitHub title would be over 255 characters"]:::ready
I3920["I3920<br/>The GitHub projection reads every issue, and a listing shorter than the repository refuses rather than reads as missing"]:::ready
I1401 --> I1402
I1401 --> I1403
I1401 --> I1405
I1402 --> I1403
I1402 --> I1404
I1404 --> I1403
I1404 --> I1407
I1404 --> I1408
I1405 --> I1407
I1406 --> I1407
classDef done stroke:#16a34a,fill:#dcfce7,color:#14532d,stroke-width:2px
classDef active stroke:#2563eb,fill:#eff6ff,color:#1e3a8a,stroke-width:2px
classDef verify stroke:#7c3aed,fill:#f5f3ff,color:#4c1d95,stroke-width:2px
classDef ready stroke:#0891b2,fill:#ecfeff,color:#164e63,stroke-width:2px
classDef planned stroke:#0891b2,fill:#ecfeff,color:#164e63,stroke-width:2px
classDef blocked stroke:#b45309,fill:#fffbeb,color:#78350f,stroke-width:2px
classDef cancelled stroke:#6b7280,fill:#f9fafb,color:#374151,stroke-width:2px
classDef superseded stroke:#6b7280,fill:#f9fafb,color:#374151,stroke-width:2pxLinked from this diagram (12)
- I1401 The identity of a projected record is what the record carries, not what its title says
- I1402 A finding says which of six states it found, and the state that could hide a person's edit is no longer one of them
- I1403 Backfill the projection once, deliberately, and record what it created
- I1404 The drift check is a gate, so a projection that stops being applied cannot stay silent
- I1405 A pull request names the issue it realises, and the edge is derived rather than remembered
- I1406 The plan's derivations are capabilities, so every surface reads the readiness the terminal reads
- I1407 One issue travels the whole path and back, as an executable case
- I1408 The documents stop claiming a projection nobody runs
- I1516 The adapter composes a projected body in one place
- I2300 A head is judged against what it changes, not against GitHub's live state
- I2844 plan validate refuses a milestone whose GitHub title would be over 255 characters
- I3920 The GitHub projection reads every issue, and a listing shorter than the repository refuses rather than reads as missing
Canonical record: .ai/repo/project/milestones/work-graph-github.yaml. Read it back with majordomus plan show work-graph-github.