Repository navigation
Implement ADR-0007: collapse per-role caller stubs into one agent-ingress.yml per repo #1723
Description
Implements the decision recorded as ADR-0007 (#1710, accepted) and reviewed in Discussion #1711: collapse each repo's per-role Class-1 event-driven thin caller stubs (dev-lead.yml, pr-review-mention.yml, ...) into a single .github/workflows/agent-ingress.yml with one job per role -- each job carrying its own permissions: block and its own pinned first-party channel ref (the ADR-0002 ring pin moves one tier down, from a per-file channel tag to a per-job pin). The ADR deliberately kept implementation OUT of the ADR PR; this epic is that implementation, scoped to the tooling + docs in .github-private and the FIRST repo's collapse. Fan-out to the remaining consumer repos (per scripts/lib/consumer-manifest.json) is a separate, human-gated follow-on.
ADR-0001 (thin-caller/reusable two-tier) is extended, not superseded: the one place ADR-0007 loosens it is that a job-level if: may act only as an event filter (github.event_name / github.event.action / payload predicate); anything reaching for repo state is logic and stays in the reusable. That loosening is the surface validate-caller-inputs must keep statically checkable.
Carve-outs (unchanged, keep their own stubs): pull_request_target roles (add-to-project, dependabot-automerge, dependabot-rebase), Class-2/3 timers, and non-agentic CI/gate workflows.
Sequencing strategy. To keep every story a single, self-contained, PR-sized unit AND avoid the broken intermediate state the ADR warns about ("in the same change"), every guard/monitor/validator story is made backward-compatible -- it accepts BOTH the pre-collapse per-role stubs AND the new ingress form -- so it can land before the collapse. The actual collapse (Story 6) then lands atomically once all guards understand the ingress shape.
Success metric (how we know it worked). For the first collapsed repo, the N Class-1 event-driven thin-caller-stub files collapse into the ingress form (one agent-ingress.yml, or a small permission-profile-grouped set per OQ1) with ZERO dropped triggers: every pre-collapse on: subscription is still served and every role still routes to its correct pinned reusable -- proven by validate-caller-inputs (forwarded inputs subset of declared at EACH per-job pin) and validate-interaction-model (every collapsed role still classifies correctly against the union on: block) both green. Secondary: a trigger change to one collapsed role becomes a one-file edit, and run attribution stays role-resolvable (job names carry the role) so the fleet monitor loses no per-role visibility.
Cost / run-count bound. This initiative changes static workflow files and shell tooling; the artifact itself adds no new per-run LLM spend. The operative bounds: (1) rollout is capped at a SINGLE repo's collapse in this epic -- fan-out is a separate human-gated initiative; (2) billed Actions runs on the collapsed repo must not exceed the pre-collapse per-role run baseline -- union-subscribed jobs whose event doesn't match are if:-skipped (a skipped job bills ~0 min), verified against a 1-week run-count baseline; (3) the existing per-PR automation budget (MAX_PR_AUTOMATION_CYCLES=10, scripts/lib/pr-automation-budget.sh, interaction-model section 9) continues to cap agent-on-agent runaway unchanged -- this epic introduces no new numeric circuit breaker.
Untracked prerequisites
- The ADR-0007 doc must land first: docs(adr): ADR-0007 — one agent-ingress stub per repo, not one stub per role #1710 (the 'docs(adr): ADR-0007' pull request, currently open) must be merged/accepted before Story 1 freezes the ingress structure. Tracked as a PR, not an issue, so it cannot be a blocked_by dependency edge (GitHub's blocked-by API is issue-only) -- surfaced here instead.
- Org-admin update to each collapsed repo's branch-protection ruleset: rename the required status-check names from the old per-workflow check names to the new per-job (role-bearing) check names, applied atomically with the collapse PR. Not automatable by the workflow GITHUB_TOKEN (repository-ruleset admin scope); a maintainer must perform it in lockstep with merge, or protection blocks merges on a check that no longer exists.
- A 1-week baseline of billed Actions run counts per role on the first collapse-target repo, captured before Story 6, so the post-collapse run-count bound (union-subscribed jobs must not increase billed runs) is measurable.
Planned from idea discussion #1711 by the BMAD Scrum Master initiative-planner. Inert until a maintainer adds initiative:auto.
Open questions — resolved 2026-09-08 (before initiative:auto)
OQ1 — exactly one agent-ingress.yml, or a small permission-profile-grouped set?
Decided: exactly one, as ADR-0007 binds. The ADR is the decision of record and the alternative was already weighed there. The pilot (#1729) is the evidence that should settle it for real; if the answer changes, that is ADR-0008 superseding ADR-0007, not an edit. Story #1724 freezes the template on exactly-one.
OQ2 — which repo is the FIRST collapse target?
Decided: petry-projects/markets (a leaf consumer). .github-private was rejected: only ~2–3 of its Class-1 workflows are true thin callers, so it would have produced a green pilot proving almost nothing. Full reasoning, the cross-repo execution constraint, and the ContentTwin fallback are recorded in #1729.
OQ3 — required-check rename sequencing across all 8 consumer repos without a merge freeze?
Still open, and deliberately out of scope for this epic. This epic collapses ONE repo. #1729 must record what the single-repo rename actually cost as the input to the fan-out initiative.
QA Lead test-risk review — folded into the stories
A test-risk advisory was requested from the QA Lead persona. The persona runtime failed to deliver it (HTTP 403 on the posting credential — see #1734); the advisory was produced manually from the same prompt and skill and posted on this epic, then folded into the story bodies as acceptance criteria — not left in comments, since dev-lead reads the body.
The load-bearing finding: "remains backward-compatible" as originally written was satisfiable by a guard that silently no-ops on the ingress form. #1725, #1726, #1727 and #1728 each gained a negative/mutation AC requiring the guard to FAIL on a known-bad ingress fixture. #1724 gained an AC forcing the ambiguous if: rulings to be decided up front. #1729 gained an observed-run-per-role AC, a pre-merge gate plus tested rollback for the required-check rename (the single escalated item), and a hard dependency on the committed run-count baseline.
Cost-bound correction (2026-09-08, from ADR-0007 review)
The bound above says union-subscribed non-matching jobs are if:-skipped and "a skipped job bills ~0 min". That is true for ONE of the two skip kinds and false for the other; ADR-0007 was corrected on this point during review (PR #1710) and the epic follows it:
- Job-level
if:event-filter skip — evaluated on the Actions service, never assigned a runner. No minutes. This is the common case and the bound holds. - Changed-path check inside the reusable — the job has already started and holds a runner for the diff step before skipping the role work. Real, if small, minutes. Any collapsed role that relied on an event-level
paths:filter falls here, becausepaths:is per-file and cannot survive the unionon:block.
So collapsing a path-filtered role trades event-level filtering for a per-event runner cost that paths: avoided entirely. The bound is unchanged in magnitude but is no longer assumed: #1729's AC10 requires the two kinds to be measured separately against the captured baseline.
Activity
don-petry commentedon Sep 8, 2026
@petry-projects/qa-lead please assess test risk on this initiative and give us your testing inputs before we hand it to
initiative-driver.Context: this epic implements ADR-0007 (#1710) — collapsing each repo's per-role Class-1 caller stubs into one
agent-ingress.ymlwith one job per role. Six stories, #1724–#1729. Guard/monitor stories are deliberately made backward-compatible so they land BEFORE the collapse (Story #1729) rather than with it.Where I most want your lens:
Proving "zero dropped triggers." The epic's success metric is that every pre-collapse
on:subscription is still served and every role still routes to its correct pinned reusable. Today that leans onvalidate-caller-inputs+validate-interaction-modelbeing green. Is static green sufficient evidence, or do we need an observed run per collapsed role before the story may close? A collapse that reduces file count but silently stops a role from firing must read as a failure, not a partial success.The
if:-as-event-filter checker ([Phase 1] Author the agent-ingress.yml canonical structure and freeze the if:-as-event-filter boundary #1724, [Phase 2] Make caller_stub_freeze and validate-caller-inputs ingress-aware (job-scoped freeze + if:-as-event-filter check) #1725). This is the one place ADR-0007 loosens ADR-0001, so it is the highest-value thing to get under test. ADR-0004 wants pure logic + bats. What does the negative-case set need to cover so the check cannot be trivially satisfied — which "logic in disguise" expressions would you insist appear as failing fixtures?Backward-compatible guards ([Phase 2] Make caller_stub_freeze and validate-caller-inputs ingress-aware (job-scoped freeze + if:-as-event-filter check) #1725, [Phase 2] Retarget fleet drift/remediate tooling to per-job extraction (never resurrect a collapsed stub) #1726, [Phase 2] Retarget run attribution to job/role level so the collapse doesn't blind the fleet monitor #1727). Each guard must accept BOTH the pre-collapse per-role stubs and the ingress form. That doubles the fixture matrix. Is there a test-shape that keeps both forms honest without the suites drifting apart, and what is the risk that a guard passes on the ingress form while silently no-op-ing on it?
The required-check rename ([Phase 3] Collapse the first repo's Class-1 event-driven stubs into agent-ingress.yml and rename the required checks #1729). A human ruleset edit must land atomically with the collapse PR. What is the pre-merge verification you would require so we do not discover a lost required check after protection is already pointing at a name nothing produces?
Anything you would add as an explicit AC on any of [Phase 1] Author the agent-ingress.yml canonical structure and freeze the if:-as-event-filter boundary #1724–[Phase 3] Collapse the first repo's Class-1 event-driven stubs into agent-ingress.yml and rename the required checks #1729 that is currently implied but not testable.
Advisory input only — no changes needed from you. I will fold what you give us into the story bodies before labelling the epic
initiative:auto.don-petry commentedon Sep 8, 2026
QA Lead — test-risk advisory (manual stand-in)
Risk tier: HIGH for #1729, MEDIUM for #1725–#1728, LOW for #1724. #1729 deletes live trigger surfaces and mutates branch protection in a repo that is serving real work; its failure mode is silent loss of automation plus a protection gap. The guard stories are medium because they are the only thing standing between #1729 and that failure mode.
The one finding that matters most: "backward-compatible" is currently untestable as written
#1725 AC4, #1726 AC4, #1727 AC3 and #1728 AC4 all assert the guard "remains backward-compatible — still passes on the pre-collapse per-role stubs." A guard that silently no-ops on the ingress form also still passes on the old form. Passing is not evidence of checking, so as written every one of these ACs is satisfiable by a guard that does nothing for the new shape.
Each of those four stories needs a paired negative/mutation AC: a known-bad ingress fixture that the guard must FAIL on. #1725 AC5 already gets this half-right ("a repo-state
if:fails") — that pattern should be lifted to all four. Concretely, the assertion set per guard should be: passes on legacy form, passes on valid ingress, fails on mutated ingress, and fails for the right reason (assert the message, not just the exit code).Highest-leverage gaps, in order
[Phase 3] Collapse the first repo's Class-1 event-driven stubs into agent-ingress.yml and rename the required checks #1729 AC2 proves the wrong property. "
validate-caller-inputsandvalidate-interaction-modelare GREEN" is static evidence — it proves the file parses and every pin resolves. It does not prove a role still fires, which is exactly what the epic's success metric demands. Add an AC: one observed post-collapse run per collapsed role, linked on the issue, before the story may close. Without it a dropped trigger is indistinguishable from success, and the metric is unverifiable.The required-check rename has no pre-merge gate and no rollback. [Phase 3] Collapse the first repo's Class-1 event-driven stubs into agent-ingress.yml and rename the required checks #1729 AC3 says the rename is "coordinated ATOMICALLY" — that is an intention, not a check. It needs: (a) capture the current required-check name set before the PR; (b) assert the post-collapse job names produce exactly that set, or a reviewed explicit mapping; (c) a stated recovery for the two half-landed states (rename without collapse, collapse without rename), since the admin step is human and the merge is not. Right now nothing describes what un-breaks the repo if those two land out of order.
The ambiguous
if:cases should be decided in [Phase 1] Author the agent-ingress.yml canonical structure and freeze the if:-as-event-filter boundary #1724, not discovered in [Phase 2] Make caller_stub_freeze and validate-caller-inputs ingress-aware (job-scoped freeze + if:-as-event-filter check) #1725. The event-filter boundary is the one place ADR-0001 is loosened, so its fixture set must be adversarial, not illustrative. Must-fail candidates:vars.*,secrets.*,needs.*.outputs.*,hashFiles(...),github.repository == …,github.event.repository.default_branch. The genuinely contested one iscontains(github.event.pull_request.labels.*.name, …)— it is payload-shaped but is really repo state that a human mutated. Decide it explicitly in [Phase 1] Author the agent-ingress.yml canonical structure and freeze the if:-as-event-filter boundary #1724 AC2 and encode it as a fixture; leaving it to interpretation guarantees the two guards disagree.The doubled fixture matrix will drift ([Phase 2] Make caller_stub_freeze and validate-caller-inputs ingress-aware (job-scoped freeze + if:-as-event-filter check) #1725 AC5, [Phase 2] Retarget fleet drift/remediate tooling to per-job extraction (never resurrect a collapsed stub) #1726 AC5). Two independent suites for legacy and ingress forms diverge under delivery pressure. Prefer one parameterised table of
(form, input, expected)driving both, so a newly added case must be answered for both forms or fail to run. This is cheaper now than reconciling two suites later.Nothing tests the ADR's own top-stated cost. ADR-0007 names total per-repo blast radius as the cost it is least comfortable with, and no story has an AC for a malformed ingress file. Add to [Phase 2] Make caller_stub_freeze and validate-caller-inputs ingress-aware (job-scoped freeze + if:-as-event-filter check) #1725: a syntactically invalid or schema-invalid
agent-ingress.ymlmust be caught by CI before merge. As it stands, the failure mode the ADR flags as worst is the one with no test.The 1-week run-count baseline is a timing dependency that will get skipped. It is listed as an epic prerequisite and [Phase 3] Collapse the first repo's Class-1 event-driven stubs into agent-ingress.yml and rename the required checks #1729 AC5 depends on it, but nothing blocks on its existence. Make the captured baseline a committed artifact that [Phase 3] Collapse the first repo's Class-1 event-driven stubs into agent-ingress.yml and rename the required checks #1729 references by path — otherwise AC5 gets closed with "looks fine."
NFR note
"Union-subscribed non-matching jobs are
if:-skipped (a skipped job bills ~0 min)" is asserted in the epic's cost bound and again in #1729 AC5. Union subscription multiplies started runs; measure it against the baseline rather than assuming it. Low severity — but it is load-bearing for the cost cap, so it should be a measurement, not a premise.Escalate? Yes — one item.
#1729 as written couples an irreversible protection change to a code change with no tested rollback path. Everything else here is an AC-sharpening. That one is a design gap: split the rename verification into its own reviewable step, or add an explicit tested recovery, before this story is handed to auto-implementation.
Advisory only — no changes made. Risk-based test strategy per
bmad-tea; I have not manufactured risk on the parts of this plan that are already well-covered (the Phase-1/Phase-2 sequencing that lands guards before the collapse is genuinely the right call, and #1727 AC4's parity fixture is exactly the right instinct).docs(adr): distinguish the two skip kinds in ADR-0007's billing claim
docs(adr): ADR-0007 — one agent-ingress stub per repo, not one stub p…
don-petry commentedon Sep 12, 2026
ADR-0007 merged to `main` as b5ebdeb (PR #1710, 2026-09-10). The epic's untracked prerequisite "the ADR-0007 doc must land first" is now satisfied, so the gate opens.
Adding `initiative:auto`. Per the DAG, `initiative-driver` should release #1724 ([Phase 1] Author the agent-ingress.yml canonical structure and freeze the if:-as-event-filter boundary) first — it is the only story with no `blocked_by` edge. The four Phase-2 guard stories (#1725, #1726, #1727, #1728) unblock on its close, and #1729 (the `markets` pilot collapse) is gated on all four.
Both open questions were resolved before this gate: OQ1 stays exactly-one ingress per ADR-0007, and OQ2 named `petry-projects/markets` as the first collapse target. QA Lead's test-risk inputs are already folded into the story bodies as acceptance criteria.
don-petry commentedon Sep 16, 2026
Initiative driver — stale in-flight slot(s) detected
The following open sub-issue(s) still carry the
dev-leadlabel but havehad no dev-lead activity for ≥ 72h, so they are occupying an in-flight
slot without progressing: #1727, #1728.
No labels were changed. A human should decide whether to split-and-close, retry,
or park each one so the slot is freed for the rest of the epic.
don-petry commentedon Sep 21, 2026
Initiative driver — stale in-flight slot(s) detected
The following open sub-issue(s) still carry the
dev-leadlabel but havehad no dev-lead activity for ≥ 72h, so they are occupying an in-flight
slot without progressing: #1728.
No labels were changed. A human should decide whether to split-and-close, retry,
or park each one so the slot is freed for the rest of the epic.
don-petry commentedon Oct 4, 2026
Initiative driver — stale in-flight slot(s) detected
The following open sub-issue(s) still carry the
dev-leadlabel but havehad no dev-lead activity for ≥ 72h, so they are occupying an in-flight
slot without progressing: #1729.
No labels were changed. A human should decide whether to split-and-close, retry,
or park each one so the slot is freed for the rest of the epic.