You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The committed roadmap.yaml AGGREGATE re-creates the conflict the fragments removed — every fragment-minting PR regenerates it, adjacent ids conflict in the merge-queue group build (#3637 UNMERGEABLE behind #3631; 18 such PRs armed tonight) #3650
docs/roadmaps/roadmap.yaml is a GENERATED aggregate of docs/roadmaps/entries/*.yaml (#3297) — the fragments made PRs pairwise disjoint on the roadmap, and check_roadmap_fragment_required.sh + roadmap_fragments.py aggregate --check require the committed aggregate to equal the regeneration. So every PR that mints a fragment ALSO commits a regenerated roadmap.yaml, and two such PRs conflict textually in it whenever their entries are adjacent in sort order — which, for ids minted the same night, is always.
Measured in the merge queue at 03:21Z: #3637 (adds PMAT-3604 + PMAT-3637) went UNMERGEABLE, head=null when the queue built its group on top of #3631 (adds PMAT-3627) — the PR itself is MERGEABLE against main; the cumulative group build conflicted in roadmap.yaml. Tonight 15 armed PRs + 3 queued regenerate roadmap.yaml (every PR that minted its AD-04 registration fragment), so the queue will dequeue each one after the previous lands, its owner re-merges with the union driver (-c core.attributesFile), CI re-runs ~22 jobs, and it re-queues: serialization at roughly one PR per hour on a saturated pool.
The fragments solved the SOURCE conflict; committing the aggregate re-created it one file over.
Fix (either, decided in the PR)
Don't commit the aggregate.roadmap.yaml becomes a build product: make roadmap-aggregate writes it to target/ (or it is gitignored), every reader (pmat work, the guards, roadmap_trim.py) reads the aggregate from the generator, and aggregate --check disappears with the file. The fragments are the source of truth already.
Keep it committed, regenerate it at the boundary. The PR's own aggregate is never committed; a post-merge job on main (push event, not merge_group — a group cannot commit) regenerates and commits the aggregate. Between merges the committed aggregate lags by one PR, which every consumer must tolerate (pmat work status reads the fragment path first).
Option 1 is simpler and removes a guard; option 2 keeps git log roadmap.yaml readable. Either way ci_resolve_dirty.sh (#3623) stops being the daily remedy for a self-inflicted conflict.
done_when
two PRs that each add one fragment, built cumulatively in a merge group, do not conflict — mutation: the fixture pair that conflicts today (PMAT-3604+PMAT-3637 vs PMAT-3627) merges clean
pmat work status <id> resolves a fragment-only entry on a branch with no committed aggregate (option 1) or with a one-PR-stale aggregate (option 2)
check_roadmap_fragment_required.sh, check_roadmap_diff_additive.sh, check_roadmap_sorted.sh, check_roadmap_ids_unique.sh judge the fragments (their universe) and the case tables say so
Refs #3297, #3623, #3643 (its sibling: the other per-merge CI-round tax). 0.70 — but tonight's queue is paying it.
Taking this. First finding, and it constrains option 1: pmat work reads the committed aggregate, not the fragments.
Measured on origin/main @ c0ab699b0, in a clean worktree:
state of the tree
pmat work status PMAT-3627
aggregate present, 55 fragments present
rc 0
aggregate removed, same 55 fragments present
rc 1
aggregate restored
rc 0
So "don't commit the aggregate; generate it wherever it is needed" is viable, but wherever it is needed is a specific and partly external list. pmat is not ours to change: it takes the monolith or it does not resolve a ticket.
Who needs it, enumerated
consumer
why it matters
pmat work status/show/add
external tool. Every ticket lookup anywhere
quorum-review.sh
refuses to launch without pmat work status <id> — measured tonight when PMAT-3602 did not exist. Every AD-04 lane launch would need the aggregate generated first
scripts/check_kind_labels_derived.sh
reads it as RM (KIND_ROADMAP overridable)
scripts/check_roadmap_completion_is_cited.sh
reads it
scripts/check_roadmap_fragment_required.sh
reads it
scripts/check_roadmap_diff_additive.sh
its premise is diffing the committed file — "a change to roadmap.yaml must come with a fragment" is not a statement you can make about a file that is not committed
The last row is the interesting one: it is not a consumer to be migrated, it is a guard whose subject disappears under option 1. Its job would become "a fragment change must regenerate cleanly", which is make roadmap-aggregate-check — already present. So option 1 probably retires that guard rather than porting it, and that should be a deliberate line in the design, not a discovery at review.
What I am checking next
Whether a generated-on-demand aggregate can be made cheap enough to sit in front of every pmat work call, or whether it wants a git hook / direnv shim — and what that does to a fresh clone, where the first pmat work would find nothing.
Whether KIND_ROADMAP-style overrides already give the other three guards a path that does not need the committed file.
The fresh-clone and CI-checkout cases specifically, because that is where "generate it wherever needed" usually breaks: a consumer nobody listed, running before any generate step.
Two things I am NOT doing
Not landing before the 0.69 tag, per the ruling. Deleting the aggregate from main mid-queue forces one more conflict on every armed PR the day before freeze, which is the churn this issue exists to end, paid twice.
No.roadmap-aggregate appears only in the Makefile; no workflow calls it. CI uses the committed file as-is
Two of those change the shape of the decision.
pmat work is not a CI consumer at all. It is local tooling plus quorum-review.sh. So the external dependency I flagged as the main constraint does not touch the required checks — it touches developers and AD-04 lane launches, both of which run on a machine that can spend 111 ms.
Nothing generates the aggregate in CI today, so the guards that read it are reading a committed artifact. Under option 1 they need one make roadmap-aggregate ahead of them — one step, in a job that already exists.
make roadmap-aggregate becomes a prerequisite of the targets that need it, rather than something remembered.
One generate step in CI ahead of the roadmap guards.
A post-checkout / post-merge hook for the local pmat work path, alongside the existing pmat hooks install.
check_roadmap_diff_additive.sh is retired, not ported — its subject stops existing, and make roadmap-aggregate-check already makes the statement that survives ("a fragment change regenerates cleanly").
The residual gap, stated rather than hidden
A fresh clone, before hooks are installed, has no aggregate — so the first pmat work status fails with rc 1 and an error that does not say why. That is real and it is the one thing option 1 cannot fully remove, because pmat is external and takes the monolith or nothing.
It is also the ordinary condition of every generated artifact, and it is 111 ms from fixed. What it needs is not a mechanism but a message: whatever we add should fail with "run make roadmap-aggregate", not with a bare lookup miss. An error that names its own fix is the difference between a five-second bootstrap and a twenty-minute one, and a silent rc 1 here would land on someone at exactly the moment they have least context.
I would not take option 2 to avoid this. Option 2 keeps a 691 KB generated file in git so that GitHub's driverless merge has something to conflict on — which is the churn this issue exists to end, paid every day, to avoid a one-line bootstrap message.
Still holding to the ruling
Lands after the 0.69 tag. Deleting the file from main mid-queue forces one more conflict on every armed PR the day before freeze — the exact cost this issue is meant to remove, charged twice.
The python3 constraint (#3649): the CI generate step adds no new surface, and here is the evidence rather than the inference
The instruction is not to add a fresh python3 step to ci.yml — the sovereign-ci image has no python3 by design. The generate must ride an interpreter an existing check already uses.
Where it goes: the guard-tree job, inside scripts/guard_tree.sh --no-cargo, which is the one step that runs every cargo-free guard — including the four roadmap guards. That job is runs-on: [self-hosted, Linux, clean-room] — intel, yoga or gx10.
I did not accept "the check passes there, so python3 must be present"
That is the reasoning that has burned this repo before: clean-room runners have no jq, and rc 127 read as GREEN — a guard can pass on a runner that lacks its own dependency, if the missing binary lands somewhere the status is not read. "It is green there" is not "the tool is there".
So I checked whether the guard's MAIN path — not its --self-test, which starts at line 274 — actually executes python3, and whether anything reads the status:
scripts/check_roadmap_fragment_required.sh
if ! out=$(python3 "$PY_LIB" changed --base … --head …); then # status READ
out=$(python3 "$PY_LIB" aggregate --check --roadmap … --entries …)
The first is a status-checked invocation on the ordinary path. A missing python3 gives 127, if ! catches it, and the guard fails loudly rather than passing vacuously. It does not fail in CI today.
That is the measurement: not "the job is green", but "the job is green and a green run requires a status-checked python3 to have succeeded on it." Those are different claims and only the second licenses putting a generate step there.
There is also no command -v python3 fallback anywhere in that guard — so there is no degraded path that could be quietly taken instead.
Consequence for the design
The CI generate rides guard_tree.sh, in the job whose guards already depend on python3 on that exact runner class. Zero new python surface, and on #3649's ledger it is not a new row — it is the same interpreter the roadmap guards already require, used once more.
If #3649 ever removes python3 from clean-room, the four roadmap guards break with it, generate step or not. That coupling exists today and this change does not deepen it — worth stating so the ledger records the dependency accurately rather than counting it twice.
For the PR body when this lands, per the review conditions
mutation RED: remove the generate step → the roadmap guards read a stale or absent aggregate and fail.
control GREEN: with it, guard-tree passes on a tree where the aggregate is gitignored.
one step, in an existing cargo-free block; no matrix, trigger or gate logic touched; additive.
Measured cost, 2026-09-14 → 09-21, for the priority of this row (cop, 06:05Z). Of 694 PR CI runs, 223 (32%) ran on a main-merge or regeneration head: 111 update-branch, 66 manual main-merges, 46 regen commits. Directly conflict-caused is 112 (16%) at minimum. At ~68 runner-min per run (19.2 jobs, 12-run sample), that is ~127–253 runner-hours per week. Merge queue: 146 merges against 19 merge_conflict removals (13%), plus 54 manual removals. The pool is the bottleneck, so this is roughly 16–32% of each ~4-day train (≈0.6–1.3 days), and the rate has risen since every PR started minting a fragment. Covers all four committed derived files (roadmap.yaml, census.json, contracts.nt, the README count); this issue is the first. Commands: gh api repos/paiml/aprender/actions/runs?event=pull_request&created=>… (head_commit.message) and GraphQL RemovedFromMergeQueueEvent.reason.
Parked by APR-EPIC-001: over E1 budget (rank 23/93). Reopen = pull. Applied by aprender-d1: claude+agy both said close, apr shadow row recorded (handoff/close-quorum-2026-09-25.json).
Activity
Merge origin/main into roadmap/PMAT-3604 — roadmap.yaml regenerated f…
merge origin/main into PMAT-3587-nextest-profile-no-fail-fast — roadm…
merge origin/main into fix/pathonly-devdeps-guard-tomllib-env — roadm…
merge origin/main into PMAT-3629-fleet-utilization-epipe — roadmap.ya…
Merge origin/main into roadmap/PMAT-3604 — aggregate regenerated again (
merge origin/main — roadmap.yaml regenerated from the merged fragment…
merge origin/main — roadmap.yaml regenerated from the merged fragment…
merge origin/main — roadmap.yaml regenerated from the merged fragment…
style(timing): rustfmt the regression test I added — the pushed head …
merge origin/main — roadmap.yaml regenerated from the merged fragment…
merge origin/main — both conflicted files are GENERATED; regenerated,…
merge origin/main — both conflicted files are GENERATED; regenerated,…
merge origin/main (second pass) — main advanced 5 commits while the f…
noahgift commentedon Sep 21, 2026
Taking this. First finding, and it constrains option 1:
pmat workreads the committed aggregate, not the fragments.Measured on
origin/main@c0ab699b0, in a clean worktree:pmat work status PMAT-3627So "don't commit the aggregate; generate it wherever it is needed" is viable, but wherever it is needed is a specific and partly external list.
pmatis not ours to change: it takes the monolith or it does not resolve a ticket.Who needs it, enumerated
pmat work status/show/addquorum-review.shpmat work status <id>— measured tonight whenPMAT-3602did not exist. Every AD-04 lane launch would need the aggregate generated firstscripts/check_kind_labels_derived.shRM(KIND_ROADMAPoverridable)scripts/check_roadmap_completion_is_cited.shscripts/check_roadmap_fragment_required.shscripts/check_roadmap_diff_additive.shroadmap.yamlmust come with a fragment" is not a statement you can make about a file that is not committedThe last row is the interesting one: it is not a consumer to be migrated, it is a guard whose subject disappears under option 1. Its job would become "a fragment change must regenerate cleanly", which is
make roadmap-aggregate-check— already present. So option 1 probably retires that guard rather than porting it, and that should be a deliberate line in the design, not a discovery at review.What I am checking next
pmat workcall, or whether it wants a git hook /direnvshim — and what that does to a fresh clone, where the firstpmat workwould find nothing.KIND_ROADMAP-style overrides already give the other three guards a path that does not need the committed file.Two things I am NOT doing
I will bring option 1 vs 2 back as a recommendation with the fresh-clone answer, not as a question.
noahgift commentedon Sep 21, 2026
Fresh-clone and CI measured. Recommendation: option 1, and the cost is smaller than the consumer list suggested.
Measured on
origin/main@c0ab699b0:python3 scripts/lib/roadmap_fragments.py aggregate --write. No cargo, no network, no toolchaindocs/roadmaps/entries/and python3. Nothing elsepmat work?grep -rn "pmat work" .github/workflows/*.yml→ nothingroadmap-aggregateappears only in the Makefile; no workflow calls it. CI uses the committed file as-isTwo of those change the shape of the decision.
pmat workis not a CI consumer at all. It is local tooling plusquorum-review.sh. So the external dependency I flagged as the main constraint does not touch the required checks — it touches developers and AD-04 lane launches, both of which run on a machine that can spend 111 ms.Nothing generates the aggregate in CI today, so the guards that read it are reading a committed artifact. Under option 1 they need one
make roadmap-aggregateahead of them — one step, in a job that already exists.Recommended shape
docs/roadmaps/roadmap.yamlleaves git, gitignored.make roadmap-aggregatebecomes a prerequisite of the targets that need it, rather than something remembered.post-checkout/post-mergehook for the localpmat workpath, alongside the existingpmat hooks install.check_roadmap_diff_additive.shis retired, not ported — its subject stops existing, andmake roadmap-aggregate-checkalready makes the statement that survives ("a fragment change regenerates cleanly").The residual gap, stated rather than hidden
A fresh clone, before hooks are installed, has no aggregate — so the first
pmat work statusfails with rc 1 and an error that does not say why. That is real and it is the one thing option 1 cannot fully remove, becausepmatis external and takes the monolith or nothing.It is also the ordinary condition of every generated artifact, and it is 111 ms from fixed. What it needs is not a mechanism but a message: whatever we add should fail with "run
make roadmap-aggregate", not with a bare lookup miss. An error that names its own fix is the difference between a five-second bootstrap and a twenty-minute one, and a silent rc 1 here would land on someone at exactly the moment they have least context.I would not take option 2 to avoid this. Option 2 keeps a 691 KB generated file in git so that GitHub's driverless merge has something to conflict on — which is the churn this issue exists to end, paid every day, to avoid a one-line bootstrap message.
Still holding to the ruling
Lands after the 0.69 tag. Deleting the file from main mid-queue forces one more conflict on every armed PR the day before freeze — the exact cost this issue is meant to remove, charged twice.
noahgift commentedon Sep 21, 2026
The python3 constraint (#3649): the CI generate step adds no new surface, and here is the evidence rather than the inference
The instruction is not to add a fresh
python3step toci.yml— the sovereign-ci image has no python3 by design. The generate must ride an interpreter an existing check already uses.Where it goes: the
guard-treejob, insidescripts/guard_tree.sh --no-cargo, which is the one step that runs every cargo-free guard — including the four roadmap guards. That job isruns-on: [self-hosted, Linux, clean-room]— intel, yoga or gx10.I did not accept "the check passes there, so python3 must be present"
That is the reasoning that has burned this repo before: clean-room runners have no
jq, andrc 127read as GREEN — a guard can pass on a runner that lacks its own dependency, if the missing binary lands somewhere the status is not read. "It is green there" is not "the tool is there".So I checked whether the guard's MAIN path — not its
--self-test, which starts at line 274 — actually executes python3, and whether anything reads the status:The first is a status-checked invocation on the ordinary path. A missing
python3gives 127,if !catches it, and the guard fails loudly rather than passing vacuously. It does not fail in CI today.That is the measurement: not "the job is green", but "the job is green and a green run requires a status-checked
python3to have succeeded on it." Those are different claims and only the second licenses putting a generate step there.There is also no
command -v python3fallback anywhere in that guard — so there is no degraded path that could be quietly taken instead.Consequence for the design
The CI generate rides
guard_tree.sh, in the job whose guards already depend on python3 on that exact runner class. Zero new python surface, and on #3649's ledger it is not a new row — it is the same interpreter the roadmap guards already require, used once more.If #3649 ever removes python3 from clean-room, the four roadmap guards break with it, generate step or not. That coupling exists today and this change does not deepen it — worth stating so the ledger records the dependency accurately rather than counting it twice.
For the PR body when this lands, per the review conditions
guard-treepasses on a tree where the aggregate is gitignored.Lands after the 0.69 tag.
noahgift commentedon Sep 21, 2026
Measured cost, 2026-09-14 → 09-21, for the priority of this row (cop, 06:05Z). Of 694 PR CI runs, 223 (32%) ran on a main-merge or regeneration head: 111 update-branch, 66 manual main-merges, 46 regen commits. Directly conflict-caused is 112 (16%) at minimum. At ~68 runner-min per run (19.2 jobs, 12-run sample), that is ~127–253 runner-hours per week. Merge queue: 146 merges against 19
merge_conflictremovals (13%), plus 54 manual removals. The pool is the bottleneck, so this is roughly 16–32% of each ~4-day train (≈0.6–1.3 days), and the rate has risen since every PR started minting a fragment. Covers all four committed derived files (roadmap.yaml,census.json,contracts.nt, the README count); this issue is the first. Commands:gh api repos/paiml/aprender/actions/runs?event=pull_request&created=>…(head_commit.message) and GraphQLRemovedFromMergeQueueEvent.reason.Merge origin/main into fix/3658-tier-squash — roadmap.yaml regenerated (
sovereign-ci: roadmap-fragment-parity arms only once the aggregate is…
noahgift commentedon Sep 25, 2026
Parked by APR-EPIC-001: over E1 budget (rank 23/93). Reopen = pull. Applied by aprender-d1: claude+agy both said close, apr shadow row recorded (handoff/close-quorum-2026-09-25.json).
release(0.70): batch-1 — 9 receipted rows, one CI run, one queue slot (…
release(0.69.1): batch 1 — 20 receipted rows + 3 combination fixes, o…