Overview
config/repos.yaml declares required_workflows per group, and heart/checks/ci_status.py's rollup() scores a repo over exactly those workflows. A required workflow with no runs at all is not scored as a failure — it simply never satisfies all_green, so the repo sits at {"conclusion": "", "status": "in_progress"} forever: able to go red, never able to go green, and never CI-clean to the readiness gate. On the dashboard that is indistinguishable from a run is in flight, which is why the state survives unnoticed.
One workspace repo sat in exactly that state from whenever it joined its group until 2026-08-24, when a human noticed the asymmetry. A survey of all 15 repos across the four groups that gate found it was the only instance, and it has since been fixed — so this is not a bug hunt. It is a guard against the hole reopening, which it does on every edit to config/repos.yaml: adding a repo to a group, or a workflow to a group's required_workflows, silently makes repos un-greenable and produces no red anything.
Plan
- Add a Heart check that asserts, for every repo in a group with
required_workflows, that a workflow exists whose name matches each required entry.
- Read the workflow list, not the runs payload — it is the only source that separates "no workflow file" from "file exists, has never run on
main". The second is genuinely pending and must stay pending.
- Match on each workflow's
name field, never its filename: name is what ci_status matches runs against, so a filename-based check could pass while the roll-up still starves.
- Classify a missing file as YELLOW and as a configuration finding rather than red CI — the repo's code is fine, its gate is not wired up, and red would misattribute the fault.
- Degrade honestly: no
gh (the web/mobile session) skips the check; a per-repo fetch error is an unknown, never an implied pass.
Detailed implementation plan
Affected Repositories
- PyAutoHeart (primary, and only)
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoHeart |
main |
clean |
Suggested branch: feature/required-workflow-file-drift
Cost — why this is one extra call, not a new tier
The original proposal assumed either a periodic Actions-API sweep or a check that runs where sibling checkouts exist. Reading heart/checks/ci_status.sh says otherwise: it already loops every polled repo in parallel making two cheap gh api metadata calls each (actions/runs?branch=main, commits/main). A third — GET /repos/{owner}/{repo}/actions/workflows — is the same shape and the same cost, and only repos in a group that gates need it (17 today; advisory groups are skipped).
Implementation Steps
heart/checks/required_workflow_drift.py — new module, mirroring manifest_drift.py's shape (own sidecar, available: false when it cannot run, coloured one-line main()):
polled_repos(config_path) — {owner, name, group} from the repos: block.
gating_repos(config_path) — those whose group declares required_workflows, with that list; reuses ci_status.load_required_workflows rather than duplicating the mapping.
fetch_workflow_names(owner_name) — gh api repos/<o>/<n>/actions/workflows?per_page=100 --jq '.workflows[].name', bounded by a timeout, stderr collapsed to one line.
check_one(repo) — {name, group, required, present, missing, error}.
run() — shutil.which("gh") guard, ThreadPoolExecutor fan-out, writes $HEART_STATE_DIR/required_workflow_drift.json.
heart/state.py — aggregate the sidecar into state.json.
heart/readiness.py — a YELLOW leg per missing workflow (required_workflow_drift), plus a stale reason naming any repo whose workflow list could not be read (required_workflow_unknown) so a missing gate cannot hide behind a 403. Scoring weights for both keys.
heart/tick.sh — run it, || heart_log WARN like its siblings.
tests/test_required_workflow_drift.py + four cases in tests/test_readiness.py.
Key Files
heart/checks/required_workflow_drift.py — new; the check
heart/readiness.py — the YELLOW leg, the stale leg, _WEIGHTS
heart/state.py — sidecar aggregation
heart/tick.sh — tick wiring
tests/test_required_workflow_drift.py, tests/test_readiness.py — 15 new tests
Testing
Full suite must stay green (python3 -m pytest -q -n auto). The suite is stdlib + PyYAML only and must not reach the network, so every fetch is monkeypatched. The tenant-firewall gate (repos_sync.py --only "tenant firewall (organ code)") also runs on Heart PRs: the new files must name no instance fact, so no allowlist entry is needed.
Out of scope
- Folding the finding into
ci_status's own sidecar or rollup() return. rollup() is the release gate; changing its shape to carry a configuration finding would ripple through readiness, the dashboard and publish for no gain, and risks colouring a missing gate as CI state.
- The release-ci profile's
unobserved list, whose sentence is specifically about dev-box-local evidence. This check is not dev-box-local.
Original Prompt
Click to expand starting prompt
Heart cannot see a required workflow that has no file — the silent never-green defect
Type: feature
Target: pyautoheart
Repos:
- @PyAutoHeart
Difficulty: medium
Autonomy: supervised
Priority: normal
Status: formalised
Filed: 2026-08-27
PyAutoHeart/config/repos.yaml declares required_workflows per group, and
heart/checks/ci_status.py's rollup() scores a repo over exactly those
workflows. A required workflow that has no runs at all is not scored as a
failure — it simply never satisfies all_green, so the repo sits at
{"conclusion": "", "status": "in_progress"} forever: able to go red, never
able to go green, and invisible to the readiness/release gate as CI-clean.
That is indistinguishable, on the dashboard, from "a run is in flight". Nothing
anywhere says "this repo is missing a gate".
This was found the hard way: autocti_workspace sat in that state from whenever
it was added to the workspaces group until 2026-08-24, when a human noticed the
asymmetry (a red Smoke Tests still reported failure correctly, so the repo could
go red but never green). Fixed for that instance by autocti_workspace#30; the
class was left as a proposal, deliberately, so the fix PR was not widened. This
is that proposal, filed.
Why it is worth a check even though the class is currently empty
A survey of all 15 repos across the four groups carrying required_workflows
(run 2026-08-24, on the issue) found autocti_workspace was the only
instance, and #30 removed it. So this is not a bug hunt — it is a guard against
the hole reopening, which it does on every edit to config/repos.yaml:
- adding a repo to a group silently makes it un-greenable until it carries a file
for each of that group's required workflows;
- adding a workflow to a group's
required_workflows silently does the same to
every repo in the group at once.
Both edits look harmless in review. Neither produces a red anything.
Shape
A cheap per-repo assertion: for every repo in a group with required_workflows,
a workflow exists whose parsed name: field matches each required entry. Match
on name:, not the filename — name: is what ci_status matches against, so a
filename-based check could pass while the roll-up still starves.
Two design notes, one of which corrects the original proposal:
- It probably needs no new data source, and no sibling checkouts. The
original proposal assumed either a periodic Actions-API sweep or a check that
runs where the workspace checkouts exist. But heart/checks/ci_status.sh
already loops every polled repo in parallel and makes two cheap gh api
metadata calls each (actions/runs?branch=main, commits/main). A third —
GET /repos/{owner}/{repo}/actions/workflows, which returns every workflow
file with its name — is the same shape and the same cost, and it is the
only call that can tell missing file from file exists, has never run on
main. Check whether that fits the <30 s tick budget before falling back to a
deep/on-demand tier; ci_status.sh's own header argues the existing two calls
are cheap enough, so a third plausibly is too.
- A missing file is a configuration finding, not red CI. The repo's code is
fine; its gate is not wired up. Colouring it red misattributes the fault.
heart/checks/manifest_drift.py is the precedent to mirror — a drift check
that classifies as YELLOW ("hygiene that will eventually break something, not
an immediate release blocker"), writes its own sidecar, and is consumed by
readiness.py as a caution rather than a gate.
The required_workflows block in config/repos.yaml already carries each
group's filenames in a trailing comment (# smoke_tests.yml + navigator_check.yml),
which is a useful cross-check but not the matching key.
Acceptance
- Adding a repo to a group whose required workflows it does not have produces a
named finding, not a silent in_progress.
- The finding distinguishes "no workflow file" from "workflow exists, no runs on
main HEAD yet" — the second is genuinely pending and must stay pending.
- A test in
PyAutoHeart/tests/ alongside test_manifest_drift.py /
test_ci_status.py.
Context
Overview
config/repos.yamldeclaresrequired_workflowsper group, andheart/checks/ci_status.py'srollup()scores a repo over exactly those workflows. A required workflow with no runs at all is not scored as a failure — it simply never satisfiesall_green, so the repo sits at{"conclusion": "", "status": "in_progress"}forever: able to go red, never able to go green, and never CI-clean to the readiness gate. On the dashboard that is indistinguishable from a run is in flight, which is why the state survives unnoticed.One workspace repo sat in exactly that state from whenever it joined its group until 2026-08-24, when a human noticed the asymmetry. A survey of all 15 repos across the four groups that gate found it was the only instance, and it has since been fixed — so this is not a bug hunt. It is a guard against the hole reopening, which it does on every edit to
config/repos.yaml: adding a repo to a group, or a workflow to a group'srequired_workflows, silently makes repos un-greenable and produces no red anything.Plan
required_workflows, that a workflow exists whosenamematches each required entry.main". The second is genuinely pending and must stay pending.namefield, never its filename:nameis whatci_statusmatches runs against, so a filename-based check could pass while the roll-up still starves.gh(the web/mobile session) skips the check; a per-repo fetch error is an unknown, never an implied pass.Detailed implementation plan
Affected Repositories
Branch Survey
Suggested branch:
feature/required-workflow-file-driftCost — why this is one extra call, not a new tier
The original proposal assumed either a periodic Actions-API sweep or a check that runs where sibling checkouts exist. Reading
heart/checks/ci_status.shsays otherwise: it already loops every polled repo in parallel making two cheapgh apimetadata calls each (actions/runs?branch=main,commits/main). A third —GET /repos/{owner}/{repo}/actions/workflows— is the same shape and the same cost, and only repos in a group that gates need it (17 today; advisory groups are skipped).Implementation Steps
heart/checks/required_workflow_drift.py— new module, mirroringmanifest_drift.py's shape (own sidecar,available: falsewhen it cannot run, coloured one-linemain()):polled_repos(config_path)—{owner, name, group}from therepos:block.gating_repos(config_path)— those whose group declaresrequired_workflows, with that list; reusesci_status.load_required_workflowsrather than duplicating the mapping.fetch_workflow_names(owner_name)—gh api repos/<o>/<n>/actions/workflows?per_page=100 --jq '.workflows[].name', bounded by a timeout, stderr collapsed to one line.check_one(repo)—{name, group, required, present, missing, error}.run()—shutil.which("gh")guard,ThreadPoolExecutorfan-out, writes$HEART_STATE_DIR/required_workflow_drift.json.heart/state.py— aggregate the sidecar intostate.json.heart/readiness.py— a YELLOW leg per missing workflow (required_workflow_drift), plus a stale reason naming any repo whose workflow list could not be read (required_workflow_unknown) so a missing gate cannot hide behind a 403. Scoring weights for both keys.heart/tick.sh— run it,|| heart_log WARNlike its siblings.tests/test_required_workflow_drift.py+ four cases intests/test_readiness.py.Key Files
heart/checks/required_workflow_drift.py— new; the checkheart/readiness.py— the YELLOW leg, the stale leg,_WEIGHTSheart/state.py— sidecar aggregationheart/tick.sh— tick wiringtests/test_required_workflow_drift.py,tests/test_readiness.py— 15 new testsTesting
Full suite must stay green (
python3 -m pytest -q -n auto). The suite is stdlib + PyYAML only and must not reach the network, so every fetch is monkeypatched. The tenant-firewall gate (repos_sync.py --only "tenant firewall (organ code)") also runs on Heart PRs: the new files must name no instance fact, so no allowlist entry is needed.Out of scope
ci_status's own sidecar orrollup()return.rollup()is the release gate; changing its shape to carry a configuration finding would ripple through readiness, the dashboard and publish for no gain, and risks colouring a missing gate as CI state.unobservedlist, whose sentence is specifically about dev-box-local evidence. This check is not dev-box-local.Original Prompt
Click to expand starting prompt
Heart cannot see a required workflow that has no file — the silent never-green defect
Type: feature
Target: pyautoheart
Repos:
Difficulty: medium
Autonomy: supervised
Priority: normal
Status: formalised
Filed: 2026-08-27
PyAutoHeart/config/repos.yamldeclaresrequired_workflowsper group, andheart/checks/ci_status.py'srollup()scores a repo over exactly thoseworkflows. A required workflow that has no runs at all is not scored as a
failure — it simply never satisfies
all_green, so the repo sits at{"conclusion": "", "status": "in_progress"}forever: able to go red, neverable to go green, and invisible to the readiness/release gate as CI-clean.
That is indistinguishable, on the dashboard, from "a run is in flight". Nothing
anywhere says "this repo is missing a gate".
This was found the hard way:
autocti_workspacesat in that state from wheneverit was added to the
workspacesgroup until 2026-08-24, when a human noticed theasymmetry (a red Smoke Tests still reported failure correctly, so the repo could
go red but never green). Fixed for that instance by autocti_workspace#30; the
class was left as a proposal, deliberately, so the fix PR was not widened. This
is that proposal, filed.
Why it is worth a check even though the class is currently empty
A survey of all 15 repos across the four groups carrying
required_workflows(run 2026-08-24, on the issue) found
autocti_workspacewas the onlyinstance, and #30 removed it. So this is not a bug hunt — it is a guard against
the hole reopening, which it does on every edit to
config/repos.yaml:for each of that group's required workflows;
required_workflowssilently does the same toevery repo in the group at once.
Both edits look harmless in review. Neither produces a red anything.
Shape
A cheap per-repo assertion: for every repo in a group with
required_workflows,a workflow exists whose parsed
name:field matches each required entry. Matchon
name:, not the filename —name:is whatci_statusmatches against, so afilename-based check could pass while the roll-up still starves.
Two design notes, one of which corrects the original proposal:
original proposal assumed either a periodic Actions-API sweep or a check that
runs where the workspace checkouts exist. But
heart/checks/ci_status.shalready loops every polled repo in parallel and makes two cheap
gh apimetadata calls each (
actions/runs?branch=main,commits/main). A third —GET /repos/{owner}/{repo}/actions/workflows, which returns every workflowfile with its
name— is the same shape and the same cost, and it is theonly call that can tell missing file from file exists, has never run on
main. Check whether that fits the <30 s tick budget before falling back to a
deep/on-demand tier;
ci_status.sh's own header argues the existing two callsare cheap enough, so a third plausibly is too.
fine; its gate is not wired up. Colouring it red misattributes the fault.
heart/checks/manifest_drift.pyis the precedent to mirror — a drift checkthat classifies as YELLOW ("hygiene that will eventually break something, not
an immediate release blocker"), writes its own sidecar, and is consumed by
readiness.pyas a caution rather than a gate.The
required_workflowsblock inconfig/repos.yamlalready carries eachgroup's filenames in a trailing comment (
# smoke_tests.yml + navigator_check.yml),which is a useful cross-check but not the matching key.
Acceptance
named finding, not a silent
in_progress.main HEAD yet" — the second is genuinely pending and must stay pending.
PyAutoHeart/tests/alongsidetest_manifest_drift.py/test_ci_status.py.Context
PyAutoMind/complete/2026/08/autocti-workspace-navigator-check.md— theinstance, and the roll-up table that shows the defect.
— the original proposal and the full 15-repo survey.