The watch for unchecked heads counted any merge-check status as checked, pending
included, so a head left with a pending mesh/merge-gate, or never given
mesh/repo-check, waited unable to merge with nothing saying so. gitea_open_pulls
now gives each head status with its state and time, and the watch says a head
whose required check stays pending or unset past the delivery table's bound for
a check asked and not answered.
The second review found the gap it was asked to look for: everything
after the forge's merge could throw — a forge 5xx, a timeout, a commit
named without parents — and the caller got a bare error that never said
the merge had happened.
From the merge on, a read-back that fails or cannot be done is said
beside merged: true, in read_back; only a proven other head is an error,
and that error says the merge happened. A parent named without its sha
is a failed read-back, never an empty head said to have landed.
A fast-forward's commit is taken as the head only when the forge names
it so. That it does is assumed, not seen on this forge, so another sha
is said in read_back rather than raised as another head having landed.
The method's description lists all five methods and says which two can
be read back. The fast-forward test now gives the merge commit a
different second parent, so reading it as a merge would fail there.
The review of #193 approved it with five nits, all taken.
Seven characters of a sha can be matched by a commit made to match them
in seconds, so an author who force-pushes could have a review merge a
commit nobody read. A head is now named by at least 12 characters, up to
64 for a sha-256 repository.
What landed was read from the forge's record of the pull request, which
is the very thing a race leaves behind. It is now read from the merge
commit: its second parent for a merge, the commit itself for a
fast-forward. A squash or a rebase makes new commits that name no head,
and the answer says it cannot be read back instead of staying silent.
A merge of another head than the one handed over was reported as a
field beside merged: true, which a caller reading only that would miss.
It is now an error that says the merge happened and what landed.
The refusal no longer asserts that the head moved after the review,
which it cannot know; it says the head is not the one named.
mesh-controller#197 was reviewed at 065cfa0d, force-pushed to 3ced96f
at 12:14 UTC, checked green there at 12:22, and merged at 3ced96f at
12:26. The merge tool kept its promise that only a checked head is
merged, but it took no argument for the head the review approved, so it
merged whatever the head was and a commit nobody reviewed landed.
gitea_merge_pull_request now takes `head`, the commit the caller
reviewed (a sha, or at least its first 7 characters), and refuses with
nothing sent when it is missing, is not a commit, or is not the pull
request's head — naming the head it has now. The forge is handed the
same head as head_commit_id, so a push between the read and the merge
is the forge's to refuse. The answer reads back what was merged and says
so loudly if the forge merged another head anyway.
`head` is required, not optional: an optional guard is the shape that
let this happen. A caller that leaves it out is told what to name.
mesh/merge-gate pass: builds build-agent, gitea, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did witho…
Every merge to the controller's repository rebuilt the route proxy and the
build seat's holder, because their builds read that repository whole. Named,
the build seat hands each only its program's build source and the planner
moves each only when that changes. A rename was said under its new name
alone, so a file moved out of a build source changed nothing the planner
could see.
After merging #159 (issue 347): every /repos/<owner>/<name> the client builds
now takes its owner and name through one helper, whether they came from a
caller or from the forge's answer; the open-pulls fixture's shas are commits.
The confirmation review of 2026-10-09 found the dedicated tools building paths
from caller text: a branch named .. became DELETE /repos/o/r/ (the repository),
a file path ../../../../version reached /api/v1/version, and a sha with
?sudo= set a status as any user. Each caller-supplied segment now goes through
one helper refusing empty, . and .. and / ? # %; a sha must be a commit; and
the client judges every path once more as it sends it: no dot or empty
segment, encoded or not, and only the allow-listed query keys.
The confirmation review asked for the delivery domain's own vocabulary
instead of a provider standing. A gitea health check would need a `tools`
list (turning gitea's tools into seat verbs), a check of another kind
beside it, and would hold the forge's own deliveries at the first-node
gate; so the delivery's owner says it, with the word it already has.
- gitea_open_pulls (gitea): every open pull request from the forge itself,
with what its base requires and the statuses its head has.
- mesh-delivery asks it every five minutes and keeps its own first-seen
time per head (the forge's update time moves on every comment). A head
on a base requiring mesh/merge-gate OR mesh/repo-check (hq requires only
the second) with neither for ten minutes is a `stalled` line in state
`unannounced`, which the controller raises as delivery.<id>.stalled (D14);
H2 has nothing to close. Once announced it is a delivery of its own.
- The gitea-side watcher and its provider standing are gone.
The confirmation review of 2026-10-09 measured GET /user?sudo=… answer the
operator's own account: the query went to the forge unjudged, and the tools'
token is the site administrator's, so any caller could act as any user.
gitea_api now sends only allow-listed query keys and refuses sudo in any case,
and a review that approves or requests changes is the operator's.
The announcer read only the repositories whose update time moved since its
last pass, less a minute. Opening, reopening or retargeting a pull request
does not move a repository's update time, so a pull request opened more
than a minute after its push was never announced, never checked, and
nothing said so (mesh-sdk #14, 2026-10-09).
- Each pass also looks at every repository holding a pull request updated
since the floor, by the forge's own search; every ten minutes, or when the
search fails, at every repository.
- Every five minutes the forge is asked for every open pull request; one on
a base requiring mesh/merge-gate whose head has had none for ten minutes
is said as the forge failing that pull request's merge check (a provider
standing, ADR 0224), which the controller keeps as a condition, and is
recovered when a check arrives or it closes.
- The client refuses a pull request or issue number that is not one before
asking the forge (a misnamed tool argument sent pulls/NaN).
Follow-ups of the review of 2026-10-09: the escape hatch sent the raw path it
had judged in normalised form; a pull request's base could be moved through
it; the merge tool would merge into a branch no rule guards; a rule named for
a branch could undercut the glob rule guarding it; and the delete tool took
any unguarded repository. It now takes only a repository of the tools' own
user with no protection and no commit in 30 days, named in full.
gitea_api took any endpoint with a token minted for the forge's
administrator with write:admin: an agent could drop a branch's protection
or its required checks, set the merge check's status, write to main or
merge its own pull request (novox/hq ADR 0266 §7, the review of
2026-10-09). gitea_api now reads anything but the administration and
writes only what an author writes; protection is only strengthened; a
merge needs every required status a success and merges only that head; a
guarded repository is not deleted; and the token is minted without
write:admin, a kept one re-minted at the next start.
It had not been run before a merge: its expected list lacked gitea_commit_statuses since that tool came,
and now gitea_contains. The catalogue's TypeScript check, merged in from #151, runs it.
Merge order on a protected trunk was the whole proof; the review asked for ancestry. The gitea module
gains gitea_contains, a read-only compare of two commits (total_commits 0 is contained), and mesh-delivery
asks it once per pair, off the lock like its plan ask, keeping the answer on the delivery. A forge that
cannot say leaves the merge held, not covered. Merge order stays the cheap first filter.
A later delivery now carries a module only when its own check passed (so a merge held without a passing
check is closed only by one that passed), its walk is of its own merge commit on the same trunk, and its
steps say the module reached every machine running it, not one. The words say what is known: a later
merge that contains it was delivered.
A recheck asks of the head the forge already judged, and the forge keeps
the newest status of each context on it. Nobody reset mesh/merge-gate or
mesh/repo-check, so the old green stood and branch protection would merge
on it while the fresh check ran; rechecked heads did turn red.
mesh-delivery's recheck now asks the forge's holder to set the gate, and
the repository check when the head holds one, to pending ("checking
again: <why>") before it asks the controller; a forge that cannot be told
refuses the recheck whole. The gitea holder takes pending, and only
pending, on the merge check's statuses by hand: no verdict, so issue 293
still holds.
mesh/merge-gate pass: builds docker, gitea, lab, nftables, slack, systemd → ace, g14, novox, shanks; no bus step; 4 wait(s) for a person; every machine com…
An agent meets the mesh's words most often in tool descriptions, and
nothing compared them with the glossary: several still said "the host"
for the node-engine and the forge's pull request comment was headed
"Change plan", a word retired twice over. retired-words is the copy of
the words the glossary retires for the tools, and checks/words fails the
repository check when any string a module's code can show, or any
manifest description, uses one. Those found are reworded here.
Reading a pull request's mesh/merge-gate and mesh/repo-check meant scraping the controller's journal:
the forge clips each status to 140 characters and nothing returned the verdict whole. mesh-delivery
already keeps the verdict; it now keeps the machine that ran it, the layers' modules and the report,
and its checks verb joins that to the forge's statuses, read through a new gitea_commit_statuses tool
that also says whether the base's protection lets the commit merge.
after: novox/mesh-controller
Branch protection requires mesh/merge-gate (and mesh/repo-check on the core
repositories) with no admin override, and the forge combines warning as a
failure. A note is now a success that says it; a repository without a
merge-check.sh is a success where repo-check is not required and a failure
for a person where it is; the status tool refuses the merge check's contexts.
One module answers 'did my change go out' for a commit and orders a
cross-repository change: one compiled state table, its state on the bus,
every transition said, noted on the commit and shown on the pull request.
The forge's holder gains the note, view and status tools it asks with, and
says closed pull requests and a merge's head and statuses.
The gate's status says what the change does and how it was judged; a change that builds
something gets its plan as a comment — the tiers, what each machine receives, and what
is not an ordinary send.
The controller asks its planner what a pull request reaches, as if merged: a module whose
manifest the change deletes is one the merge removes, and the plan's dependents are said
on the gate's status beside the modules it moves.
The controller now decides what a pull request's check runs from the mesh's module
graph, so the announcement carries the changed directories that hold a module at the
head and whether the head has a merge-check.sh. A verdict sets mesh/merge-gate (the
gate, with the modules it judged) and mesh/repo-check (the repository's own tests).
gitea_branch_protection_get/set let the operator's agent make those statuses required.
The catalogue's merge-check.sh leaves the gate to the build seat and keeps its own layer:
every manifest through module check, and the touched Go modules' tests.
The forge's announcer announces each new head of an open pull request as pull.updated, once,
and marks the head pending; the controller asks the build seat to check it against every
machine of the mesh's facts, and says the verdict as checked, which the forge's holder sets as
the head commit's status mesh/merge-gate - an error never as a success - with the check's own
account as a comment when it is not a pass. merge-check.sh is the catalogue's check: every
manifest through the running controller's module check and merge gate, and the Go tests of each
module the change touches, a module whose dependencies cannot be fetched said as not tested.
The controller read a changed file as shared code unless its directory was a module it holds or
the merge also changed that directory's manifest. A merge touching modules/showcase/index.ts -
the catalogue's reference module, held by no machine - therefore rebuilt all 103 modules built
from this repository on 2026-10-06, 88 of them byte-identical, with the build agent first only
because everything else is built by it.
Whether a directory is a module is a fact of the repository at the commit, so the announcer now
looks it up: every directory above a changed file (never the root) is asked for its module.json
at the merge commit, and the ones that have one go out as module_dirs, with module_dirs_said.
Not said when the file list is cut, past 300 directories, or when the forge cannot be asked;
the controller then keeps its old rule, which rebuilds too much rather than too little.
modules/showcase stays: TestTheShowcaseModuleIsAValidManifest in mesh-controller parses it and
hq to-be 18 and 20 name it as the reference module.
With a gate on the first machine and a rollback after it, a module's build rolls out by
default. The ones kept back say why: the network path a rollback could not cross, the
providers every consumer on a machine drops with, and the stores holding the photos.
A merge's deleted files are announced, so a module whose manifest went is forgotten
rather than asked to build (the public-acme plan failure).
With the poll the only announcer of a merge, its cost showed: every 30 s it
asked every repository for its pull requests, a pass outlasted the tick, and
passes piled up beside each other — a merge was announced four and a half
minutes late, and two passes at once could each announce it. A pass now asks
only repositories updated since a minute before the last look, and the next
pass starts when this one ends. hq issue 250.
Ten tools the console lacked for the actions a review and a merge leave behind: close or reopen a
pull request whose work landed elsewhere, change its title or body, read its files, its diff and its
comments, reopen an issue, read one file at a ref, list branches, delete the branch a closed pull
request leaves. Each is the client's own call; `gitea_api` stays the escape hatch for the rest.
Tested against the fake forge through the compiled tools, the way the console calls them (13/13).
The forge's own users are the mesh's to settle — making the builder's login
a site admin so private repos build (hq 229) — and the tools' token had no
write:admin. A token kept from before a scope was added lacks it, so the
client now treats the forge's 403 "required scope" like a 401: the source
re-mints by name with the whole list and retries once. The fake forge in the
tests learns /repos/search, which the client has used since 2026-09-28 and
which had left 9 of the 11 token tests failing on main.
Deployed #49 and the watcher immediately broke: GET /user/repos answered 403,
'required=[read:user]' — confirmed live against the running forge (1.27.3).
That route sits under gitea's user scope category despite listing
repositories, not repository as assumed.
Also gives the fake forge real scope enforcement on /user/repos, which is
why the original PR's test suite didn't catch this: it only checked the
token's value was valid, never that it carried the required scope.
The runtime beside the forge served 0 tools: GiteaClient.fromEnv required a token
(settings or MESH_GITEA_TOKEN), nobody had one to give — the mesh raised the forge —
and putting one in settings would store a secret in plaintext in the inventory. So
the fifteen tools registered nothing and the watcher logged "not watching".
What the mesh does deliver is the admin account: a login the manifest names and a
password the vault minted and the host unsealed into a file (ADR 0086). That is
enough to mint a token, so the module does (hq issue 100, the forge's tools):
POST /users/{admin}/tokens over basic auth, scoped to write:repository and
write:issue — the least the tools and the repo watcher need — kept at 0600 in the
module's own state (/var/lib/mesh/gitea/state, a new directory resource the runtime
mounts writable), read back on the next start, and minted afresh when the forge
answers 401 to it or the kept file is gone. A forge whose data came from the
predecessor has no mesh-admin: that is reported in plain words on every poll until
it clears, once per reason, not crash-looped. A configured token still wins and is
never minted over.
The mint happens on the first call, not at registration: a contributor is
synchronous, and a forge not yet answering must not keep the runtime from serving.
One source per kept file in a process, or the watcher and the tools would each
renew on a 401 and drop the other's token by name.