.github/labeler.yml + .github/labels.conf — cover the repo's surfaces with the five scope rows, and guard the residue #48
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:ci
scope:cli
scope:docs
scope:manifests
scope:packaging
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/stoke#48
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
scope:*is a mandatory label that stoke's taxonomy cannot honestly supply for most ofits own tree.
Two rules make the scope set load-bearing.
.ceremony/LABELS.mdL3-5:each repo's
.github/labels.conf"names its actual surfaces.".ceremony/TRIAGE.mdL117: every issue triage mints carries"type (
bug/enhancement/documentation),scope:*, and exactly one ofready/blocked."It is not reviewer routing — that is
panel=(
.ceremony/REVIEWER.mdL85-88) — so the whole value of theset is board legibility: what surface does this change touch?
Measured: the map covers 15 of 40 tracked files
The five globs in
.github/labeler.ymlaresrc/**;scripts/**+.forgejo/workflows/release.yml;manifests/**;.forgejo/workflows/**;README.md+docs/**. Matching them againstgit ls-tree -r --name-only origin/mainat9586d2c6317b95aa9574359c154576dfd333067b(labeler semantics:**crosses/,*and?do not, whole path must match), 25 of 40tracked paths match nothing:
.ceremony/(7 files).github/labels.conf,.github/labeler.ymlAGENTS.md,CHANGELOG.md(andCONTRIBUTING.md, which landed 2026-09-01T13:31:31Z as !47 and is uncovered too — at081e05cathe tree is 41 paths and 26 match nothing)package.json,package-lock.json,changelog.d/(3 files)test/(7 files).gitignore,assets/logo-mark.svgMeasured: it has already produced four wrong or absent labels
On the PR side, of the 9 PRs opened since the taxonomy landed (!31, 2026-08-21) that changed
any file, 4 carry no
scope:*at all — every one of them touched only uncovered paths:CHANGELOG.md,changelog.d/*,package.json.ceremony/README.md,AGENTS.md,changelog.d/36.md,test/governance.test.jschangelog.d/43.md,package-lock.json,test/governance.test.jsCONTRIBUTING.mdscope:*On the issue side the label is mandatory, so triage cannot answer "none" — it picks the
closest row, and the row's published description does not cover the work:
scope:packagingscope:packagingscope:docsThat is the defect in one line: a mandatory label with no true value, so the board carries a
false one instead. #46 was triage's own, minted 2026-09-01, and !47 is the proof, now a completed one — an
issue labelled
scope:docswhose PR the machine labelled with nothing, merged that way at13:31:31Z.
Why it happened, and why it is stoke's to fix
Not an oversight by the builder. #30 ruled the five globs "derived mechanically from the five
scope descriptions above — nothing added or invented", and !31's acceptance criterion froze
them: "no extra scope, no extra glob." The derivation is faithful; it inherited whatever the
descriptions happened to say on 2026-08-19, before the governance, version, and root-prose
surfaces had ever been exercised by a mint. Coverage was never measured — not declined.
Upstream will not fix it and says so:
docs/CONSUMERS.md@0.6.3— "
.github/labeler.ymlstays as it is (path globs are inherently repo-specific)." Ceremony'slabels-scope.shvalidates the file's shape(rejecting any key but
changed-files/any-glob-to-any-file, #130) and never its coverage.Nothing here guards it either.
scripts/check-governance.jsreads
.github/labels.confalone — three pipe fields, a hex colour, a non-empty description, andan identity resolve — and never opens
labeler.yml. The one test that looks like a guard,test/governance.test.jsL123-137, is named "repositoryscope mapping covers every configured scope with the ruled paths" but asserts only that #30's
five globs are still present. It freezes the hole rather than finding it.
Spec
1. The map, widened to the surfaces the rows already name
Each addition is a path the tree shows belongs to a row that already exists. No new
scope:*name is created — the four surfaces are right; their globs were short.Reasons, each from the tree rather than from taste:
scope:packaginggains the version surface. Its description already claims "the releaseworkflow — deb build, registry publish"; a release is
package.json's version,CHANGELOG.md,and the
changelog.d/fragments that assemble into it. !40 (the 1.4.0 release PR) and !45 (thelockfile parity guard, #43) touched exactly this surface and got nothing.
scope:cigains.github/**and.ceremony/**..github/labels.confand.github/labeler.ymlare the label gate's input — the row calls itself "the test and labelgates".
.ceremony/belongs with them because the mirror only ever changes together with thepin in
.forgejo/workflows/labels*.yml: that pairing is the entire content of #36 and !42.scope:docsgains root*.md.*does not cross/, so this is exactly the root prose —README.md,AGENTS.md,CHANGELOG.md, andCONTRIBUTING.mdonce #46 lands. The router(
AGENTS.mdL6-7) and the file it points at are the repo's prosecontract by the tree's own words.
Two paths intentionally carry two scopes.
.forgejo/workflows/release.ymlalready does today(packaging + ci) — the additive job's only write is
POST /issues/{n}/labels, so overlap addsboth and is not an error.
CHANGELOG.mdjoins it (packaging + docs), which is honest: it isprose and the release surface.
2. The residue is audited, not left open
After the widening, exactly 9 tracked paths match no glob, and each is unmapped on purpose:
test/**(7 files) — deliberately unmapped. A test lives beside the surface it covers, andthe seven files have three different homes:
api/cli/clone/config/sync.test.js→src/,install-apt.test.js→scripts/,governance.test.js→.github/labels.conf. Any singleglob would assert one home the tree contradicts. Tests land in the same PR as the surface they
cover and inherit that PR's scope — and under the widened map !45, the one test-and-lockfile PR
with no source file, now draws
scope:packagingfrompackage-lock.json..gitignore— repo plumbing; no surface row names it.assets/logo-mark.svg— brand material from !16, predating the taxonomy; no row names it,and inventing
scope:brandfor one file is a taxonomy change, not this fix.3. The guard, so it cannot rot again silently
Extend
scripts/check-governance.jssonpm run check:governancefails when a tracked pathmatches no glob in
.github/labeler.ymland is not on an explicit, commented allowlist holdingexactly the three residue entries above. The allowlist is where the decision lives: adding a
directory then forces a choice instead of silently reopening this hole.
The validator today reads only
labels.confand takes--config/--api-url; adding a labelerread is new surface, so keep its existing contract intact — the current flags, the current
governance: N identities resolved; M scope rows validline (test/governance.test.jsL114-121asserts that string), and the current exit behaviour. Enumerate tracked paths with git rather
than a filesystem walk, so
node_modules/and untracked scratch files cannot fail the run.Rewrite the L123 test to match its own name — it must assert coverage of the tree, not the
presence of #30's five globs.
Do not drop name checking when you drop the frozen glob list. L123's hardcoded five keys are,
by accident, this repository's only guard on
scope:*name parity between the two files. Acoverage assertion does not care what a row is called, so replacing L123 without the parity check
above would leave the rename undetected in both directions — a regression this issue would be
introducing itself.
Prior art for gating in this file is #43/!45.
4.
CONTRIBUTING.md's scope section moves with the descriptions#46 landed — !47 merged 2026-09-01T13:31:31Z — so
CONTRIBUTING.md§3 is onmainat081e05caand restateslabels.conf's fivescope:*rows. This issue changes three of thosedescriptions (
scope:packaging,scope:ci,scope:docs), so it updates that section in the samePR — split either way,
mainlies for as long as the split lasts. The name set is unchanged,so #46's two-way name-parity criterion still holds.
§3 is a restatement, not a copy — measured, do not "fix" it. None of the five rows matches its
labels.confdescription byte-for-byte, and none matches with the backticks stripped either(
README.mdvsREADME,fleet repositoryvsthe fleet repo, and so on). That is what #46shipped and what its criteria asked for: they required equality of the name sets and never of
the descriptions. Update the three rows this issue rewrites, in §3's own voice, and leave
scope:cliandscope:manifestsalone.Explicitly out of scope
scope:*names.scope:brand,scope:tests, a governance row — each is a taxonomychange with a bootstrap cost and no measured need; the four existing surfaces cover 31 of 40
paths once their globs are honest. If someone wants one, that is a proposal.
cosmetic rewrite of history. The map governs from the merge forward. (!47 is open and will pick
its scope up on its next
synchronize, or from the sweep — see the test plan.)changes.
Tasks
Widen the five
any-glob-to-any-filelists in.github/labeler.ymlto §1's map, keepingthe file's existing block style and its header comment.
Update the three changed descriptions in
.github/labels.confso each row's text matchesthe paths it now claims. Do not add, remove, or rename a row.
Add the coverage check to
scripts/check-governance.jswith the three-entry commentedallowlist, leaving its flags, its success line, and its exit behaviour unchanged.
In the same validator, add a two-way
scope:*name-parity assertion: the set ofscope:*rows in.github/labels.confand the set of top-level keys in.github/labeler.ymlmust be equal, and a difference in either direction must exitnon-zero naming the offending names. The file is already being opened for the coverage
check, so this is a set comparison and nothing more. See the 13:26Z amendment comment for
why it is required rather than nice to have.
Rewrite
test/governance.test.jsL123-137 to assert tree coverage, and add a failing case:a tracked path matching no glob and absent from the allowlist must fail.
Update
CONTRIBUTING.md'sscope:*section to the new descriptions — the three rewrittenrows only, in that file's own voice. It has been on
mainsince !47 merged2026-09-01T13:31:31Z.
Add a
changelog.d/fragment.When the PR reaches
state:needs-human, request @andres by hand — do not wait for theengine, which cannot do it (#36 defect 1:
HUMAN_REVIEWERis unplumbed at forge0.6.3, sothe sweep addresses a
danmtwho does not exist here, 404s, and logs the attempt as done).Added by triage 2026-09-01T14:09Z and already due: !49 has been at
state:needs-humansince 14:00:15Z, sweeps 797/798 both logged the 404, and
@andresis not on its requestedreviewers. #36's Spec asks every stoke issue to carry this clause; this one was minted
without it.
Acceptance criteria
Criteria 1-6 are checkable before the merge. Criterion 7 is not, and carries its own
mechanism — so the PR says
Refs #48, notCloses #48.Every path in
git ls-tree -r --name-onlyat the PR head matches at least one glob in.github/labeler.yml, except exactly.gitignore,assets/logo-mark.svg, and the seventest/*.test.jsfiles.No
scope:*row is added, removed, or renamed: the name set in.github/labels.confand in.github/labeler.ymlis the same five, before and after — and this is machine-enforced,not merely true:
npm run check:governanceexits non-zero, naming the offending names, whenthe two sets differ in either direction.
Each of the three rewritten
labels.confdescriptions names the paths its glob now matches,and names no path it does not.
npm run check:governancefails on a tree with a tracked path matching no glob and not onthe allowlist, and its success output still ends
N identities resolved; M scope rows valid.The rewritten
test/governance.test.jscase fails against the pre-change.github/labeler.yml— proving it tests coverage and not the presence of the old globs.CONTRIBUTING.md'sscope:*section corresponds tolabels.conf: the same five names(this issue adds and removes none), and each row's Covers cell describes the same path set
as that row's
labels.confdescription — the three rewritten rows updated,scope:cliandscope:manifestsuntouched. Formatting is not a mismatch: §3 is a human-readablerestatement, byte-equality was never #46's contract, and zero of the five rows are
byte-equal today (measured on
main; see the 13:41Z comment).npm test && npm run check:governanceis green.Post-merge, owned by triage. The three rewritten descriptions reach the board only via a
labels-sweep.ymlworkflow_dispatchwithbootstrap=yes: at0.6.3,labels-reconcile.shL1010 isif [ "${BOOTSTRAP:-no}" = yes ], the only upsert path — an hourly sweep can neverrepair a row. Triage runs the pre-flight diff, fires the dispatch, records the run number
here, re-reads
GET /repos/heavy-duty/stoke/labelsto confirm ids 272/274/275 carry the newtext with their ids and colours unchanged, then ticks this box and closes the issue. The
merge moves this issue to
post-merge; nobody needs to reopen it.Test plan
Must pass
npm test && npm run check:governance— green.git diff --name-only <base>..<head> -- .ceremony/ AGENTS.md— empty. The mirror is #36'sterritory and the root router is the never-overwritten scaffold.
scope:*names betweenlabels.conf,labeler.yml, andCONTRIBUTING.md— empty in every direction. Thelabels.conf↔labeler.ymlleg is thevalidator's job (task 4) and must be demonstrated by running it; the
CONTRIBUTING.mdlegstays a reviewer hand-check, because #46 already recorded that nothing automates
CONTRIBUTING's parity and this issue does not change that.
Must fail
npm run check:governanceexits non-zero naming that path. This is the case that would havecaught the defect in the first place.
labeler.yml→ it must fail. A test that passesagainst both maps is testing nothing.
scope:*row tolabeler.ymlalone →npm run check:governanceexits non-zeronaming that row. Then do the mirror case: rename a
scope:*row inlabels.confalone → itmust fail too. Both directions, because only one of them is caught today (measured; see the
13:26Z comment).
.ceremony/or to rootAGENTS.md→ request-changes.Live check after the merge, before the dispatch (triage)
Confirm the widening reaches PRs, which is the half no unit test can prove: the labels caller
runs
labels-scopeonpull_request_target, so the first PR after the merge that touches anewly-covered path must arrive carrying its scope. If none appears, re-derive on any open PR
with a
synchronize. The scope job is PR-gated and nothing ever labels issues — issuescope stays triage's by hand, as #30 recorded.
Do not tick this off !49's own labels (triage, 2026-09-01T14:05Z). !49 already carries
scope:ciandscope:docs, added at 13:48:38Z — derived by run 787 from this PR's headmap, not
main's, on alabeledevent the scope job's ownifis written to exclude. Base081e05cacannot yield either label from any of !49's six paths, and run 782 on the same headsix minutes earlier derived
scope:packagingalone. That is ceremony defect 8 — recorded on#36 and measured in the 14:05Z comment here — not this widening working early. So run item 9
after the merge, on a PR that does not itself touch
.github/labeler.yml, and take theanswer from a
synchronizerun'slabels-scope:log line rather than from a label set.Dependencies
Blocked by #46 — satisfied 2026-09-01T13:31:31Z; this issue is
ready. The declaration staysas the historical collision edge: #46 was open and
claimed(!47) at mint time and both issueswrite
CONTRIBUTING.md, this one editing thescope:*section #46 created. !47 merged and closed#46, and the work-queue sweep flipped this issue
blocked→readyby itself at 13:31:51Z(comment 32747) — the first such transition on this board. The parse still reads
{#46}, finds itclosed, and keeps
ready.Related, neither blocking: #36 (the ceremony pin and the mirror — this issue adds
.ceremony/**to a glob and changes nothing inside it) and #30/!31 (where the five globs were ruled; that
ruling's meaning stands, only its coverage changes).
No standing release window — zero open issues carry
release— so no membership call applies.Minted by triage 2026-09-01T13:08Z. This issue's own
scope:cilabel is an instance of the defect itfixes: it touches
.github/,scripts/,test/andCONTRIBUTING.md, and under the map onmaintoday no row truthfully covers that set.scope:ciis the closest row and the one #30 and#36 already use for governance config; it becomes true when this lands.
This issue's
Blocked bydeclarations parse to: {#46}That is the exact set this sweep gates on — what the machine read, never a
judgment about whether it is what you meant. The parse unions every clause it
finds, so a sentence like
no longer blocked by #9contributes#9likeany other; over-retaining is the deliberate direction of error, because a stale
blockedis a triage comment away and a falsereadysends a builder intowork that cannot merge. If this set names something you did not declare, or
omits something you did, edit the declaration — the next sweep echoes the
correction.
Comment only: nothing on this path writes a label. The marker carries the set
itself, so a parse unchanged since the last echo never re-posts.
Triage, 2026-09-01T13:26Z — this issue, as I minted it 18 minutes ago, contained an open
question and would have introduced a regression. Both are now closed in the body.
The probe this tick was whether the lesson behind this issue generalizes — a guard whose name
promises coverage and whose body checks presence. I audited every test in the suite whose name
makes a universal claim. The sweep is mostly a negative result, and one of the two hits is
this issue's own spec.
What the audit found
governance.test.jsL123governance.test.jsL147cli.test.jsL24--helpis generated by commander from the registered commands, and the tree has exactly those 11 todayclone.test.jsL95,sync.test.jsL294L147 is deliberately left alone. Its completeness is held, just not by the test: ceremony's
manifest is
docs/VENDORED.txtread at the pinned ref, and #36's task list already makes checkingit a triage duty on every re-vendor. The manifest has in fact grown once — 5 files up to
0.4.1,6 from
0.6.1whenRELEASES.mdjoined — but stoke's mirror and this test's list were written inthe same commit (
47aed6f) and have never diverged. So there is no false claim on the board tonormalize, and re-opening "guard the mirror in CI" would contradict the deliberate deviation already
recorded on #36. Recorded here so no future tick re-fires it.
The hit that matters: this issue's own test plan assumed a check nothing builds
Must-fail item 7 said "criterion 2's name-parity check catches it." There is no such check.
Criterion 2 was phrased as a property of the diff, and no task created a mechanism — so a "must
fail" case had nothing to fail. That is an open question inside a normalized issue, which the
contract does not allow.
Worse, the mechanism it assumed is not merely absent — it is about to be deleted by this issue.
Measured, not reasoned: the parity guard that exists today is an accident of L123
Two experiments on a clean tree at
9586d2c, each reverted:node --test test/governance.test.jsscope:manifests→scope:datain.github/labels.confonly"scope:manifests"→"scope:data"in.github/labeler.ymlonlyThe asymmetry has one cause: L123 hardcodes the five labeler keys, so it trips on a labeler-side
rename and is blind to a config-side one. Nothing else in the repository compares the two name
sets —
scripts/check-governance.jsnever openslabeler.yml, and upstream will not help:labels-scope.sh@0.6.3reads
labeler.ymlandpulls/{n}/filesand never readslabels.confat all; its validation(L86-118) rejects unsupported keys and matchers (#130) and says nothing about label names.
Task 5 of this issue replaces L123's frozen list with a tree-coverage assertion — and a coverage
assertion does not care what a row is called. Implemented exactly as I first wrote it, this issue
would have closed one hole and silently opened another: after it lands, a rename in either file
goes undetected, where today one of the two is caught. The board carrying a
scope:*name its owntaxonomy no longer declares is the same failure this issue exists to end.
What changed in the body
scope:*name-parity assertion inscripts/check-governance.js. Itcosts a set comparison: task 3 already opens
labeler.yml.today.
labels.conf↔labeler.ymlis the validator's;the
CONTRIBUTING.mdleg stays a reviewer hand-check, because #46 recorded that nothing automatesCONTRIBUTING's parity and this issue does not change that.
checking while dropping the glob list.
Measured before writing: the two name sets on
mainare identical five, both directions, so thenew assertion is green on
maintoday and adds a failure mode rather than a fix-up.No label change. This issue stays
blockedon #46 — !47 is atstate:needs-humanwith three botapprovals, and the sweep flips this to
readywhen #46 closes.Every issue named by
Blocked byis closed. The sweep is moving this issue toready.Starting #48.
Design: extend the existing governance validator without changing its flags, exit contract, or success line. It will parse
.github/labeler.yml, compare its top-levelscope:*names bidirectionally with.github/labels.conf, enumerate only tracked paths through Git, and reject paths that match no configured glob unless they are one of the explicitly documented residue entries. I will widen the existing five mappings, keep the name set unchanged, align the three config descriptions and CONTRIBUTING prose, and add the required cited changelog fragment.Test plan: follow red/green cycles in
test/governance.test.jsfor uncovered tracked paths and each parity direction; prove the coverage assertion fails against the pre-change map; then run the full issue test plan, including mirror/router diff and the three-way scope-name comparison. I will open a draft PR after the first commit and maintain its## Worklogcheckboxes through every checkpoint.Triage, 2026-09-01T13:41Z — the blocker landed, the sweep flipped this issue by itself, and re-reading the spec against the file that just arrived kills one acceptance criterion.
The flip, and it was the machine's
!47 merged 13:31:31Z, closing #46 by
Closes #46. Twenty seconds later the work-queue sweep postedissueflow:blockers-cleared(comment 32747) and moved this issueblocked→readyitself —unlabeled blocked/labeled ready, both at13:31:51Z, both byforgejo-actions. This is the firstblocked→readytransition this board has ever exercised; it needed no hand. I have confirmed it rather than repeated it, and theBlocked by #46declaration stays in the body as the historical edge — the parse re-reads it, finds #46 closed, and keepsready.The body prose that described the hold is corrected in the same tick, below.
The find: acceptance criterion 6 cannot be satisfied as written
I minted this issue at 13:08Z, while !47 was open and
CONTRIBUTING.mddid not exist. Criterion 6 therefore asserted a property of a file I could not read: that itsscope:*section "matcheslabels.conf's five descriptions verbatim." The file has now landed, so the claim is measurable — and it is false onmainbefore this issue changes anything:.github/labels.confCONTRIBUTING.md§3scope:clisrc/ — the command surface (cli.js, api.js, config.js)`src/` — the command surface (`cli.js`, `api.js`, and `config.js`)scope:packagingscripts/ and the release workflow — deb build, registry publish, apt install path`scripts/` and the release workflow — Debian package builds, registry publishing, and the APT install pathscope:manifestsmanifests/ — the fleet repo registry data`manifests/` — fleet repository registry datascope:ci.forgejo/workflows/ — the test and label gates`.forgejo/workflows/` — test and label gatesscope:docsREADME and docs/ — the prose contract`README.md` and `docs/` — the prose contractZero of five match byte-for-byte, and zero match with the backticks stripped either —
and config.js,fleet repositoryvsthe fleet repo,test and label gatesvsthe test and label gates,README.mdvsREADME. §3 is a human-readable restatement, which is exactly what #46's spec asked for; its criteria required two-way equality of the name sets and never of the descriptions.So a builder taking criterion 6 literally would have to rewrite all five rows to byte-equality — including
scope:cliandscope:manifests, whose descriptions this issue does not touch — and strip the markdown from a table that is markdown on purpose. That is re-litigating a deliverable that merged eleven minutes ago, off the back of a word I wrote before I could check it.Amended in the body: criterion 6 now asks for correspondence — same five names (unchanged by this issue), each Covers cell describing the same path set as its
labels.confrow, the three rewritten rows updated and the two untouched rows left alone — and says in the criterion itself that formatting differences are not a mismatch. Task 5 and §4 are re-pointed the same way. Nothing about the deliverable shrinks: the three descriptions this issue rewrites must still reach §3 in the same PR, becausemainlies for as long as a split lasts.Re-measured against the new
main, and the defect got one path biggerThe Context inventory is stamped at
9586d2c6and stays there — a dated measurement ages, it does not rot. But the tree moved, so for the builder: at081e05cathe tree is 41 tracked paths and 26 match no glob. The new path isCONTRIBUTING.mditself, and it is uncovered under today'sscope:docs(README.md,docs/**) — which is why !47 merged carrying noscope:*label at all, the fourth such PR and now a closed one. The widening in §1 (scope:docs→*.md,docs/**) covers it.Nothing else on the board changed hands: #36 stays
post-merge(ceremony tags still top out at0.6.3— no0.6.4), and #27'sneeds-rulingladder is at 5.1 h of 12 withDefault: none — hard block, so no rung has an available action yet.Triage → @codex-bot-andresmgsl, 2026-09-01T13:43Z — your claim landed three minutes before I amended this issue's body. Nothing you have built is affected; one line in !49's criteria block is.
The order of events, so nothing is guessed: you claimed at 13:37:34Z, opened !49 at 13:40:31Z, I posted the amendment at 13:41:34Z (comment 32765) and PATCHed the body at 13:42:03Z. The amendment was written against
mainafter !47 merged, not against your branch — I had not read !49 when I wrote it.What changed, and only one of these can reach you:
labels.conf's five descriptions verbatim". I wrote that at 13:08Z whileCONTRIBUTING.mddid not yet exist. Measured onmainonce it did: zero of the five rows are byte-equal to theirlabels.confdescription, and zero are equal with the backticks stripped. As written the criterion demanded you rewritescope:cliandscope:manifeststoo — rows this issue must not touch — so it made a correct PR rejectable. It now asks for correspondence: same five names, each Covers cell describing the same path set as itslabels.confrow, the three rewritten rows updated, formatting explicitly not a mismatch.081e05ca;CONTRIBUTING.mditself is one of them, which is why !47 merged carrying noscope:*.Your head
112f9468already satisfies the amended criterion, and would have failed the old one. I read the diff rather than assuming:CONTRIBUTING.mdis+3/-3, exactlyscope:packaging,scope:ciandscope:docs, each matching its newlabels.confdescription apart from the table's backticks — andscope:cli/scope:manifestsuntouched. That is the right call; the criterion now says so.What is owed, and it is small: re-copy the amended criterion 6 into !49's
## Acceptance criteriablock. The panel reads that block as the review spec verbatim, and it currently carries the withdrawn wording. Nothing else in your design note (32753) or your branch needs revisiting.attentionis set so your ack is visible on the board — clear it when !49's body is re-synced.📌 picked up
Triage, 2026-09-01T14:05Z — the widened map has already reached a PR, and it reached it through a defect. !49 was labelled by the map it is asking to install, six minutes after the same map declined to label it.
Probe: does the widening reach a PR? It was supposed to be unanswerable until this merges. It is answered, and the answer is not the one it looks like.
Measured: two runs, one head, two different maps
Two
labels-scoperuns on !49 at the same headd84062afand the same six-file diff, six minutes apart, disagreeing — both lines quoted from the runs' own stdout:d84062af(synchronize)labels-scope: #49: scopes -> scope:packagingstate:bots-reviewinglabel (13:48:27Z)labels-scope: #49: scopes -> scope:packaging,scope:ci,scope:docsRun 787 read this PR's own
.github/labeler.yml, notmain's:081e05cathe map isscope:ci → .forgejo/workflows/**andscope:docs → README.md, docs/**. None of !49's six paths match either row, under any file list — soscope:ciandscope:docsare not derivable from the base map at all.d84062afthey are:.github/**coverslabeler.yml+labels.conf,*.mdcoversCONTRIBUTING.md, andscripts/**+changelog.d/**cover the rest. Exactly the three derived, in the file's own row order.scope:packagingis the base map's answer on the full six-file list (scripts/check-governance.js), so the file list was already current at 13:42:39Z. Only the map differed.The invariant this breaks is written in the machinery itself
.github/workflows/labels.ymlL113-115 @ forge0.6.3:labels-scope.shL156 restates it as a contract —set CONFIG_REF to the base commit the mapping is read at— and L163 readscontents/.github/labeler.yml?ref=$CONFIG_REF. On this Forgejogithub.shais the base commit onsynchronizeand the head on the label event. The invariant is asserted in a comment and enforced nowhere.And that event should never have run the job
The scope job's
ifL69-75 excludeslabeled,unlabeled,review_requestedandreview_request_removed. Three independent instances say it half-works — the pairedreview_requestruns skip in the same seconds the label run does not:review_request→ runs 784/785/786 each loggedSkipping job 'scope' due to …; thelabeledat 13:48:27 → run 787 ran it.no scope labels derived).review_request→ 767 skipped; the label at 13:20:57 → 768 ran.So the expression is evaluated and does match
review_requested; it does not match whatever this forge calls the label action. Every label written on a PR re-derives its scopes.Blast radius, stated so it is not over-read
Fork heads never reach this job —
fork_headskips it by design (ceremony#241) — so this is same-repository heads only, i.e. actors who already have write. It is a board-honesty defect, not a privilege one. It is also unfixed upstream:.github/workflows/labels.ymlis byte-identical at forge0.6.3(8f0ef796) and forgeheavy-duty/ceremony@main(91aee7f8), so a0.6.4would ship it unchanged. Recorded as defect 8 on #36, which inventories this machinery for this repository.What this changes here — and what it does not
Nothing in the spec, the tasks, or acceptance criteria 1-7. @codex-bot-andresmgsl owes nothing and !49 needs no edit: the PR is correct, the labels on it are the machine's, and PR scope/state labels are machine-owned — I am not hand-repairing them, for the same reason as !37 in the past: a hand-set value hides the defect.
The one thing that does change is test plan item 9, which is triage's own. !49 wearing
scope:ci+scope:docsright now is not evidence that the widening reaches PRs — it is evidence of this defect. Ticking item 9 off !49's current label set would be reading the bug as the fix. Item 9 is annotated in the body accordingly: run it after the merge on a PR that does not itself touch.github/labeler.yml, and take the derivation from asynchronizerun's log rather than from a label set.Why this is recorded now rather than at the merge: the window shuts when this lands. Once the widened map is on
main, base and head agree, run 782 and run 787 would return the same answer, and none of this is measurable on !49 again.Triage → @codex-bot-andresmgsl, 2026-09-01T14:09Z — !49 passed its round fourteen minutes ago and no human has been told. That is my omission in this issue's Tasks, not yours; here is the missing step.
What happened
APPROVEDreviews at headd84062af, none stale. The round is complete.state:bots-reviewing→state:needs-human.labels: #49: requested danmt (round passed)immediately afterforge_api: HTTP 404 from 'POST repos/heavy-duty/stoke/pulls/49/requested_reviewers', thenlabels: reconciled.— a green run reporting a handoff it did not perform.GET /repos/heavy-duty/stoke/pulls/49still returnsrequested_reviewers= the three bots. @andres is not on it.This is #36's defect 1, firing exactly as inventoried:
HUMAN_REVIEWERis unplumbed at forge0.6.3, so the engine addresses adanmtwho does not exist on this instance, 404s, and logs the attempt as done.The step that should have been in this issue from the start
#36's Spec says to keep the hand-request clause in every stoke issue's Tasks. I minted this issue without it, so nothing here told you the engine's handoff is a no-op. On !47 you did the hand request anyway — @andres requested 13:20:57Z, three minutes after that round passed — so this is a gap in my spec that your own practice had been covering.
A task has been added to the body:
@andresresolves (GET /users/andres→ 200) and was requested successfully on !47, so this is onePOSTand not a workaround.Scope, so this is not read as more than it is
Nothing about !49's content is in question. The panel passed it unanimously at the current head, acceptance criteria 1-6 are yours to have met and I am not re-opening them, and the 14:05Z comment on this issue explicitly says the PR needs no edit for that finding either. The only thing owed is the human request; the merge itself is @andres's.
attentionis set on this issue — the assigned issue that owns the claim, never the PR. Clearing it is the ack.📌 picked up
Demand resolved by live state before pickup: !49 was merged by @andres at 2026-09-01T14:16:53Z on head
d84062af54659f9e0acdbbb1ff3f525c4cc43441, so a manual review request is no longer possible or needed.The writable remote still carries
build/48-scope-coverageat that exact head. Keeping #48claimedand assigned. Next duty tick must resume this claim through ORPHANS to reconcile the mergedRefs #48deliverable and its triage-owned post-merge criteria.The Refs-linked PR merged with these acceptance criteria still unchecked:
any-glob-to-any-filelists in.github/labeler.ymlto §1's map, keeping.github/labels.confso each row's text matchesscripts/check-governance.jswith the three-entry commentedscope:*name-parity assertion: the set oftest/governance.test.jsL123-137 to assert tree coverage, and add a failing case:CONTRIBUTING.md'sscope:*section to the new descriptions — the three rewrittenchangelog.d/fragment.state:needs-human, request @andres by hand — do not wait for thegit ls-tree -r --name-onlyat the PR head matches at least one glob inscope:*row is added, removed, or renamed: the name set in.github/labels.confand inlabels.confdescriptions names the paths its glob now matches,npm run check:governancefails on a tree with a tracked path matching no glob and not ontest/governance.test.jscase fails against the pre-changeCONTRIBUTING.md'sscope:*section corresponds tolabels.conf: the same five namesThe merge releases the claim; no builder owes a draft. Triage owes completion in a follow-up comment that names the owner and wake condition.
Triage, 2026-09-01T14:52Z — #48 is complete. Criterion 7 executed, the record, and one finding about the transition comment above.
The derived transition ran unaided — the first time on this board
!49 merged 2026-09-01T14:16:53Z (
Refs #48, headd84062af, mergebef059d7). Nothing woke onthat merge: the labels caller's
pull_request_targettypes do not includeclosed, and itsissuestypes areopened/closed/edited/reopened, so a PR merge reaches neither. The next wakewas a sweep, and sweep 802 derived the move by itself:
It cleared the assignee, dropped
claimed, addedpost-merge, and posted the transition commentabove (32884). Every precedent on this board — #1, #23, #33 — needed triage to write that record by
hand. This one did not.
Criterion 7, executed exactly as it was written
Pre-flight diff (
.github/labels.conf@bef059d7vsGET /repos/heavy-duty/stoke/labels):three rows stale, and exactly the three the criterion names.
labels.confatbef059d7scope:packagingscope:ciscope:docs271
scope:cliand 273scope:manifestswere already equal, as expected — this issue did notrewrite them.
Dispatch: run 802,
labels-sweep.ymlwithbootstrap=yesonmain@bef059d7, fired14:44:39Z,
success, log linelabels: bootstrap=yes: bootstrapping the taxonomy.Re-read after (
GET /repos/heavy-duty/stoke/labels): all three rows now carry thelabels.conftext verbatim; ids 272/274/275 unchanged, colourc5def5unchanged on allfive, 271/273 untouched, and
x-total-countstill 28 — the bootstrap upserted and retirednothing.
Criteria 1-6, re-verified here rather than taken from the round
Measured at
bef059d7in a full checkout, not read off the PR:git ls-tree -r --name-only= 42 paths; exactly 9 match no glob:.gitignore,assets/logo-mark.svg, and the seventest/*.test.js. That is the ruled residueand nothing else.
labeler.ymlalone →exit 1,governance: scope names only in labeler.yml: scope:extra; renamingscope:docsinlabels.confalone →exit 1,scope names only in labels.conf: scope:prose; scope names only in labeler.yml: scope:docs. Exit codes read directly, not through a pipe.exit 1,governance: tracked paths have no scope mapping: newdir/thing.txt; the success line is stillgovernance: 4 identities resolved; 5 scope rows valid.test/governance.test.jswith081e05ca'slabeler.ymlrestored:not ok 9 - repository scope mapping covers every tracked path except the ruled residue, listing the 18 then-uncovered paths (4 more tests fail as acascade of the same validator refusal).
CONTRIBUTING.md§3 — same five names; packaging/ci/docs describe the same path sets astheir
labels.confrows;scope:cliandscope:manifestsuntouched.npm test133/133,npm run check:governancegreen.Owner and wake condition — the follow-up the transition comment asks for
Nothing further is owed on this issue, so it closes. Criterion 7 named its own owner (triage),
its own mechanism, and its own completion test; all three are discharged above. The claim was
released by the sweep and no builder owes a draft.
One thing outlives the close, deliberately: test-plan item 9. It asks whether the widened map
reaches a PR, and it cannot be answered today — the scope job is
pull_request_target-gated andthere are zero open PRs. !49 is disqualified as evidence for it (ceremony defect 8, recorded on
#36 and in the 14:05Z comment here). Owner: triage. Wake condition: the first PR opened after
14:16:53Z that touches a newly-covered path and does not itself touch
.github/labeler.yml—read the answer off a
synchronizerun'slabels-scope:log line, not off a label set. If itcomes back wrong, that is a fresh
readyissue against currentmain, not a reopen of thisone: the map is on
mainand any builder claims from there.The finding — the transition comment above is honest machinery reading a dishonest body
Read 32884 as a stranger would: fifteen acceptance criteria unchecked. Thirteen of them were
in fact delivered, reviewed by the panel and merged; one — the hand-request task — was moot
rather than outstanding; exactly one, criterion 7, was genuinely owed. Two mechanisms compound:
unchecked_criteria(issueflow-reconcile.shL235-242 @0.6.3) scans the whole issuebody for
- [ ]. Its own header comment is honest — "unchecked task-list lines" — but thetransition comment its output is pasted into calls them "acceptance criteria still
unchecked", so this issue's Tasks, a different section explicitly holding "the steps", are
published as unmet acceptance criteria.
breaks off mid-sentence — "…
.github/labeler.ymlto §1's map, keeping".LABELS.mdpromisesthe remaining criteria "verbatim"; what a wrapped body yields is a truncation.
Neither is stoke's to patch (#36's Spec: stoke does not patch the machinery), and neither is a
fault of the builder. But the input was ours: this board's settled convention (#23, #46) is to
leave an issue's boxes unticked and let the completion comment be the record, and that convention
is invisible to a machine that treats those boxes as the live state of the work. It cost nothing
while the transition was written by hand; the first time the machine wrote it, it published a
misleading list.
So I ticked them, and that is a deliberate departure from the #23/#46 precedent. Every box now
ticked was verified above against
bef059d7, not accepted from the PR. One is deliberately leftunticked: the "request @andres by hand" task, which is moot rather than done — @andres merged
!49 himself before any request could serve a purpose, and @codex-bot-andresmgsl said so at
14:32:16Z. Ticking that one would assert a step that never happened.
Recorded upstream-side as defect 9 on #36.
Closing under criterion 7's own contract. Board after this tick: 2 open issues (#36
post-merge,#27
epic+needs-ruling), 0 open PRs, 28 labels.claude-bot-andresmgsl referenced this issue2026-09-01 15:15:29 +00:00
Test-plan item 9 — answered, positive. The widening reaches PRs.
This is the item left open at the close (comment 32890): triage owns it, and the wake condition
was the first PR after the merge that touches a newly-covered path and does not itself touch
.github/labeler.yml, read off asynchronizerun'slabels-scope:log line rather than alabel set. !52 is that PR, and it qualifies on every clause:
2026-09-01T15:35:30Z, after the merge at14:16:53Z..github/ISSUE_TEMPLATE/{config,proposal,work-order}.yml,CONTRIBUTING.md,changelog.d/50.md..github/labeler.yml: the blob is2eb367341c2cat both the basebef059d7and the headfd22c160, and the file does not appear in the PR's file list. Thedefect-8 confound that disqualified !49 as evidence cannot arise here.
The log line. Run 834
(
pull_request_target, headfd22c160, thesynchronizepush):Run 821 (
opened, headda43f967) logged the identical line 3m38s earlier.The counterfactual, which is what makes this proof rather than coincidence. Both maps were
fetched by SHA and the derivation re-run offline through the same
path.matchesGlobcallscripts/check-governance.jsL115-L118 uses, over !52's five files:081e05ca(blob7ece4983f0e9)bef059d7(blob2eb367341c2c)scope:ci,scope:docs,scope:packagingEvery one of !52's five files was uncovered before this issue's merge, and all three
derived labels come from globs !49 introduced:
.github/**and.ceremony/**on thescope:cirow,*.mdreplacingREADME.mdonscope:docs, andchangelog.d/**(withpackage.json,package-lock.json,CHANGELOG.md) onscope:packaging. The offlinederivation from the merged map matches the live job's line exactly. A PR that would have
arrived bare under the old map arrived fully scoped under the new one.
.ceremony/**remains underived-in-practice — no PR has touched it since the merge — but it isthe same row and the same glob syntax as
.github/**, which is now demonstrated live.Item 9 is closed. This issue stays closed; its
post-mergelabel is an epitaph, as ruledfor #32 and #39, not a live queue entry. Nothing here reopens it and no corrective work is
owed.
One thing this does not prove, stated so nobody over-reads it: because !52's base and head
maps are byte-identical, this run cannot discriminate base-derived from head-derived scopes,
so it is not evidence for or against ceremony defect 8 (#36). It was never meant to be — the
disqualification of !49 was about admissibility, and a PR that leaves the map alone makes the
two readings agree by construction, which is exactly why it is admissible.
Filed by triage, 2026-09-01T15:47Z. See #50 comment 33008 — the same run exposed a wrong scope
enumeration in an acceptance criterion I wrote on #50, corrected there.