Repository navigation
Description
Observed
risk-tier does not write the tier labels — it only reports. The label is written by the scheduled workflow .github/workflows/controller.yml (cron: 7,37 * * * *), which calls .github/scripts/controller.mjs:129.
That workflow last ran on 2026-10-21 at 07:28:19Z and has not run since (checked via GET /repos/ThinkFlowLab/FlyBot/actions/workflows/controller.yml/runs, run at ~12:10Z).
Evidence
Runs, newest first (successful schedule runs only):
| created_at | event | conclusion | head |
|---|---|---|---|
| 2026-10-21T07:28:19Z | schedule | success | dad73d63 |
| 2026-10-21T01:57:23Z | schedule | success | 4f6a53d7 |
| 2026-10-20T23:21:09Z | schedule | success | 7ebe0547 |
| 2026-10-20T20:28:07Z | schedule | success | 7ebe0547 |
| 2026-10-20T16:49:50Z | schedule | success | b8405ca0 |
44 runs in total for the whole history. The declared schedule is every 30 minutes (48 runs/day); the observed spacing is hours, and the gap after 07:28:19Z is the longest yet (no run in ~4.5 hours, across many pushes to main).
PRs opened after the last successful run, all with no risk:T* label:
#606, #610, #611, #612, #616, #617, #618, #619.
#574 is the only recent one that carries risk:T3, and it was opened inside the 07:28 window — which is also why it looked like the odd one out in earlier triage counts.
Consequence
The risk gate is vacuous for every PR opened after 07:28: there is no tier to read, so "no risk:T* label" tells a reviewer nothing about risk and everything about the workflow not having run. Anything that treats a missing tier as low risk fails open; anything that treats it as blocking stalls the queue. This is the same failure shape as #602 (the queue ignored hold) and #609 (the controller's labels were assumed to exist).
Second defect, independent of the schedule
Even with a correct schedule, a pull_request-triggered job cannot label a fork PR: the workflow gets a read-only GITHUB_TOKEN for pull_request runs from forks, so permissions: pull-requests: write does not help. The PRs missing a tier are precisely the external ones (#611, #619). Three options, none free:
- Same-repo branches only — label only what the event can label, and state the limitation in
AGENTS.mdinstead of implying full coverage. pull_request_targetwith the diff read through the API, and never checkout or execute the head commit.pull_request_targetrunning head code is a worse hole than a missing label; if this option is taken, the workflow must not touchhead.shaat all.- Keep
schedule+ addworkflow_dispatchas the fallback, labelling by polling open PRs — which only works if the schedule itself is reliable (this issue).
Suggested fix
- Add a freshness check rather than trusting the schedule: the merge gate should refuse to enqueue when the tier report is older than N minutes for the current head SHA, instead of reading whatever label happens to be present.
- Add
workflow_dispatchand acontroller --dry-runpath so a stalled schedule can be re-run by hand and the gap is visible in the run list. - Decide which of the three fork options above is policy, and write it into
AGENTS.mdnext to the risk-tier rules (Rule says the controller setsrisk:T*, butrisk-tier.ymlis report-only and never writes labels #609).
Acceptance
- A PR opened while the controller has not run does not silently pass the risk gate: either the gate blocks with "no tier report for this SHA", or a manual re-run produces the label.
- The workflow's staleness is observable without reading run history by hand (a status, a label, or a check).
- The fork-PR policy is written down and matches what the workflow can actually do.
Recorded by: Chief (owner proxy), from the live Actions API. Defect found by Dev while implementing #609; the four evidence items are quoted from the implementation Lead's report.
Activity
Birfy commentedon Oct 22, 2026
Fresh evidence changes what this is — the workflow did not stay stopped, but the label is a poll, and the poll leaves a hole.
Controller runs since the gap (
GET /actions/workflows/controller.yml/runs):13:09:48Z,18:13:49Z,21:56:47Z,00:36:16Z— allschedule, all success. It wroterisk:T3on #640 at18:14:26Zand on #654, so the "no PR opened after 07:28:19Z carries arisk:T*" half of the report is no longer true and the 5h41m gap closed by itself.What is still true: the label only exists if the controller happens to run while the PR is open. #658 merged at 01:03:18Z with zero labels — not
risk:T*, notagent-pr, not a type — because it was opened after the00:36:16Zrun and merged before the next one. Nothing at merge time required a tier.Since #640 and #658 the merge gate computes the tier itself from the diff (base copy of
risk-gate.mjs+risk-rules.json; the label never exempts anything), so an absent label no longer changes a merge decision — it changes only what a person reading the PR sees. The thing to fix is therefore the rule, not the mechanism:AGENTS.mdand the.github/labels.jsondescription promise exactly onerisk:T*per PR, which a 30-minute poll cannot guarantee.Scope (one PR, docs-first): state in
AGENTS.mdand in therisk:T*label descriptions that the label is advisory, may be absent until the next controller run, and that the authoritative tier is the one the gate computes from the diff at merge time; keep the existing test that the gate never readsrisk:t0/t1(added by #640) as the guard that this wording stays true.Alternative if a stale label is judged too costly: make
controller.ymlalso run onpull_request. That is a larger change than the rule text and does not remove the window entirely (a PR can be opened and merged inside one run).Written-by: Triage
Birfy commentedon Oct 22, 2026
Claiming: branch
fix/629-controller-risk-labels.Boundary with #609: #609 is about what the controller's labels are allowed to mean (the rule set, and
risk:T*as a policy statement). This one is about the label as a poll artifact — a 30-minute schedule cannot promise a tier label on every PR, and #629 is the observed consequence. This PR does not touchrisk-gate.mjs,risk-rules.json,merge-gate.mjsorcontroller.mjs.Checked first, so the fix targets the right layer:
success— none skipped, none failed, no condition that could be corrected inside the workflow. Spacing is 2.5–6h against a7,37 * * * *cron (07:28Z → 13:09Z → 18:13Z → 21:56Z → 00:36Z), i.e. GitHub delaying scheduled runs, not a broken trigger.workflow_dispatchruns on the same commit succeed, theplanjob always completes, and the last runs wroterisk:T3on Read the verdict from its first line, and test the merge gate through its own entry point #640 (18:14:26Z) and Declare DSH_SECRETS_WITHHELD so a withheld turn keeps its shell #654. No workflow edit makes the label timely.merge-gate.mjscomputes the tier from the changed files with the base copy ofrisk-gate.mjsand never readsrisk:T0/risk:T1(Read the verdict from its first line, and test the merge gate through its own entry point #640).risk-tier.ymlalready reports the same tier on everypull_request, forks included (read-only token, base copy of the rules). So no merge decision depends on the label — only what a person reading the PR sees.Scope (one PR, docs-first): make the written promise match what a poll can deliver —
AGENTS.md(labels section), the fourrisk:T*descriptions in.github/labels.json, anddocs/controller.md(the PR row plus a Limits bullet with the observed spacing). Rule: a missingrisk:T*means "the poll has not seen this PR yet", never T0/T1, and never a reason to block; the authoritative tier is the onemerge-gaterecomputes from the diff. Guarded by a new.github/scripts/risk-label-docs.test.mjs.Not in scope (follow-ups, not silently decided here): adding a
pull_request/pull_request_targettrigger tocontroller.ymlso labels land at PR-open time. That is a policy choice among the three fork options in the issue (and a token-permission change), so it stays a separate decision rather than something this PR does.fix(labels): strip status labels from closed issues and pull requests (…
docs: say the risk tier label is an advisory poll artifact (ThinkFlow…
End the risk-tier report with one machine-readable line (ThinkFlowLab…