Repository navigation
[qa-lead S4] Make persona.yml's declared surfaces agree with what is actually deployed #1647
Description
Story
As a reader of personas/qa-lead/persona.yml,
I want its triggers.surfaces to describe what the runtime actually serves,
so that the manifest — which persona-standards §1.1 calls the index-of-record — stops advertising an interaction contract nothing honors.
Blocked by #1646, which decides which surfaces are genuinely served.
The problem
personas/qa-lead/persona.yml declares six surfaces as enabled: true, mode: advisory:
| Surface | Served? |
|---|---|
mention |
yes |
pull_request |
no (until #1646) |
pull_request_review |
no |
issues |
no |
discussion |
no (declares a repository_dispatch bridge that nothing sends) |
check_run |
no |
personas/qa-lead/interaction.yml (#1404/#1418) already records the truth — "the contract records what is actually deployed, not the aspiration" — and cites combsmith/.github#755 finding 10. So the repo now holds two files that disagree, with the honest one in the newer, less-read location. The manifest is the one the standard calls authoritative.
This is not cosmetic. §4.1's whole argument for one router plus per-persona manifests is that the manifest is where per-persona decisions live and the router restates none of them. A manifest that claims six surfaces while one is wired means any future router reading it would fire five dead paths.
Acceptance Criteria
- After [qa-lead S3] Serve the pull_request surface so qa-lead runs without being summoned #1646,
personas/qa-lead/persona.ymllists exactly the surfaces that are served asenabled: true. Every unserved surface isenabled: false— kept, not deleted, with a note recording that it is an intended future surface and what would be needed to serve it. Deleting them loses the design intent; leaving themtrueis the lie. - A validator invariant makes this class of drift impossible to reintroduce:
personas/validate-personas.pyfails when a persona declares a surfaceenabled: truethat no deployed workflow serves. The mapping from surface to serving workflow must be derived — frompersonas/<id>/interaction.yml'sworkflows[]andtriggers.events[], which already exist for exactly this purpose — not hand-maintained per persona. - The invariant is covered by
tests/test_validate_personas.bats: a persona with an enabled-but-unserved surface fails; one whose enabled surfaces are all served passes; a persona with nointeraction.ymlis handled explicitly (skip or fail — decide and document, do not crash). - The same audit is run across the other eight personas and the findings reported.
sre-lead,devops-lead, andsecurity-leadwere onboarded from the same template ([bmad-onboard S1] Onboard sre-lead persona #1307/[bmad-onboard S2] Onboard devops-lead persona #1308/[bmad-onboard S3] Onboard security-lead persona #1309) and very likely carry the identical over-declaration. Fix them in this PR if the fix is mechanical; file a follow-up if it is not. personas/qa-lead/interaction.ymlandpersona.ymlagree after the change, and a test asserts they cannot silently diverge again.
Tasks / Subtasks
- Set the unserved surfaces to
enabled: falsewith explanatory notes. (AC: test issue from agent #1) - Implement the derived surface↔workflow invariant in
validate-personas.py. (AC: Go-live improvements for PR review agent #2) - Add the bats cases, including the no-
interaction.ymlpath. (AC: Add @claude delegation, auto-merge, and rebase handling #3) - Audit the other eight personas; fix or file. (AC: Optimize review: small-PR and incremental fast paths #4)
- Add the manifest/contract agreement assertion. (AC: feat: add Copilot engine support via REVIEW_ENGINE toggle #5)
Dev Notes
validate-personas.pytoday checksid== directory ==canary.agent, theaddress.handleslug invariant, handle/alias uniqueness,definition.layers[].pathexistence,vendor_pinvsVENDOR.md, the eval split structure, and the non-draft canary registration. Adding a surface-liveness invariant fits that existing shape — cross-invariants the JSON schema cannot express.- Prefer failing closed. A surface declared
enabled: truewith no serving workflow should fail the validator, not warn. The precedent is Rename murat → qa-lead and make it @-addressable #1279's handle-collision check, which fails because a collision means "a dropped mention or a double-fire" — the same category of silent wrongness. - Do not solve this by deleting the surfaces. The
discussionsurface in particular encodes a real design decision (therepository_dispatchbridge, issue fix(engine): reorder cross-provider fallback to claude → copilot → gemini #571) that should survive asenabled: false. - Draft status does not exempt a persona from this. Eight of nine personas are
draft; ifdraftmeant "the manifest may lie," the manifest would be worthless during exactly the period it is being copied from.
Project Structure Notes
personas/qa-lead/persona.yml, personas/validate-personas.py, tests/test_validate_personas.bats, and likely the other seven personas/*/persona.yml.
References
- petry-projects/.github
standards/persona-standards.md§1.1, §4, §4.1 - Persona framework — pre-rollout review findings .github#755 finding 10
personas/qa-lead/interaction.yml,personas/validate-personas.py,tests/test_validate_personas.bats- Rename murat → qa-lead and make it @-addressable #1279 (the existing cross-invariant precedent), [Phase 2] Author a machine-readable interaction contract for every agentic role, per the standard #1404/feat: implement issue #1404 — [Phase 2] Author a machine-readable interaction contract for every agentic role, per the standard #1418 (interaction contracts)
Update — 2026-09-01, cross-session reconciliation
- A ninth persona landed:
solution-architect([Phase 2] Create thesolution-architectadvisory persona (manifest + advisory prompt + interaction contract + eval seed) #1629, merged as feat: implement issue #1629 — [Phase 2] Create thesolution-architectadvisory persona (manifest + advisory prompt + interaction contract + eval seed) #1639). The audit in AC Optimize review: small-PR and incremental fast paths #4 now covers eight other personas, not seven. It was onboarded from the same template, so expect the same over-declaration. personas/solution-architect/interaction.ymlexists and was authored under the same [Phase 2] Author a machine-readable interaction contract for every agentic role, per the standard #1404/feat: implement issue #1404 — [Phase 2] Author a machine-readable interaction contract for every agentic role, per the standard #1418 contract shape — use it as a second reference point for the derived surface↔workflow mapping in AC Go-live improvements for PR review agent #2.- Known platform gap, do not treat as a bug (tracked on Epic: Persona framework — post-rollout hardening & follow-ups #1318): the
discussionmention surface has no labels API, so<id>:hands-offcannot opt out a discussion. When AC test issue from agent #1 marksdiscussionenabled: false, record this as the reason rather than implying the bridge is merely unbuilt.
Update — 2026-09-13, budget guard (read before starting)
This issue has a real timeout risk. Sequence it so a budget overrun costs the fleet audit, never the core fix.
#1734 timed out attempting two ACs in one run and had to be split (#1775/#1776) after the work was
lost. Do not repeat that here. Implement in this order and commit as you go:
- AC test issue from agent #1–Add @claude delegation, auto-merge, and rebase handling #3 for
qa-leadonly — the manifest correction, the derived surface↔workflow invariant
invalidate-personas.py, and the bats coverage. This is the deliverable; it must be complete and
green on its own. - AC feat: add Copilot engine support via REVIEW_ENGINE toggle #5 — the manifest/contract agreement assertion.
- AC Optimize review: small-PR and incremental fast paths #4, the eight-persona audit, last. Fix only what the existing mechanical edit covers
(flippingenabled: true→enabled: falsewith the reason note). If the remaining personas need
per-persona judgment, or if you are running short on budget, stop, report the audit findings in
the PR description, and open a follow-up issue — a partial PR with an honest findings table is a
good outcome; a timed-out run that loses everything is not.
Two further constraints:
- AC Go-live improvements for PR review agent #2's invariant must not red-light the fleet on merge. Nine personas are in the tree and eight
aredraft. If the new check fails closed before those manifests are corrected, it gates every PR in
this repo — the exact outage pattern of the ADR-0006 schema change on 2026-09-07. Either correct all
nine manifests in this PR, or land the invariant and the corrections together in the same commit.
Do not land a failing-closed validator ahead of the data it validates. - The one open question is already answered: for a persona with no
interaction.yml, SKIP (do not
fail) — and log a::notice::naming the persona. Rationale:interaction.ymlis the derivation
source, and failing on its absence would punish a persona for a missing contract file rather than
for an over-declared surface, which is a different defect with its own issue. Document this in the
validator docstring and cover it with the AC Add @claude delegation, auto-merge, and rebase handling #3 bats case.
Steering reaches this agent through the issue title and body only — a question posted as an issue
comment will not be read and will not be answered. If something is still ambiguous, pick the reading
that best satisfies the ACs above, implement it, and state the assumption in the PR description.
Activity
don-petry commentedon Sep 8, 2026
Evidence: the
discussionsurface is not merely unwired — the runner explicitly refuses to postFound via a
codeant-aireview finding on PR #1689 (2026-09-08) and verified againstmain.AC #1 asks that unserved surfaces be set
enabled: falsewith the reason recorded. Fordiscussionthe reason is stronger and more specific than "the router does not wire it" — the shared runner has an explicit guard that skips the post:.github/workflows/persona-runner-reusable.yml, lines 285-286:Discussions carry no
item_number(the runner's own concurrency comment says so: "item_number is empty for discussions, so key on comment_url too"), so the guard always fires on that surface.What this means for the manifest.
personas/qa-lead/persona.ymlcurrently declares:That note describes the inbound bridge, which is real — the router does dispatch. What it omits is that the outbound path is a no-op: the agent runs, burns a model call, and posts nothing. So a discussion mention today is pure cost with zero output, which is worse than a surface that simply never fires.
Suggested AC #1 addendum: when setting
discussiontoenabled: false, record both halves — inbound bridging works, outbound posting is unimplemented — and cite the runner line above, so a future contributor re-enabling it knows the post-path is the work, not the router.Two related notes:
mentionsurface on discussions has no labels API, so<id>:hands-offcan't opt out a discussion (feat: implement issue #737 — [Phase 1] Instrument auto-rebase: agentic-conflict-resolution rate + fan-out CI-run volume report #755 residual)". Combined, discussions currently have no opt-out and no output — worth stating plainly in the manifest note.Origin: unresolved review thread on #1689, dispositioned there rather than fixed in a docs-only PR.
don-petry commentedon Sep 13, 2026
Dev-Lead Implementation Plan
Issue: #1647 — [qa-lead S4] Make persona.yml's declared surfaces agree with what is actually deployed
Scope
Correct
qa-lead's manifest so everyenabled: truesurface is one a deployed workflow actually serves, add a derived surface↔workflow invariant tovalidate-personas.py(derived from each persona'sinteraction.yml), cover it with bats, and apply the mechanical fix across the other eight personas.Derivation (AC #2)
A persona surface is served iff
personas/<id>/interaction.yml'sinteraction.triggers.events[]contains an event that serves it:pull_request→pull_request,pull_request_review→pull_request_review,issues→issues,check_run→check_run/check_suite,discussion→discussion,schedule→schedulemention→ any comment event (issue_comment/pull_request_review_comment/discussion_comment) or arepository_dispatch:*bridge (covers pr-review).No
interaction.yml→ SKIP (log::notice::, do not fail) — it is the derivation source; its absence is a different defect.Audit result (all 9 personas)
pull_request_review,issues,discussion,check_run→enabled: false+ notes.issues,discussion→enabled: false+ notes. (mechanical — fixed here)repository_dispatch:pr-review-mention) → no change.Implementation Checklist
personas/validate-personas.py: addcheck_surface_liveness(fail-closed; enabled surface with no serving event fails; no-interaction.yml skips with::notice::). Document the skip in the docstring.personas/qa-lead/persona.yml: 4 surfaces →enabled: falsewith future-intent notes (discussion note records Epic: Persona framework — post-rollout hardening & follow-ups #1318 no-labels-API + fix(engine): reorder cross-provider fallback to claude → copilot → gemini #571 unbuilt bridge).issues+discussion→enabled: falsewith notes.Tests to Write (
tests/test_validate_personas.bats)interaction.yml→ SKIP/pass +::notice::(AC Add @claude delegation, auto-merge, and rebase handling #3)repository_dispatch:*bridge → PASSTest Command
scripts/run-bats.sh tests/test_validate_personas.batsandbash .dev-lead/scripts/dev-lead-lint.shdon-petry commentedon Sep 13, 2026
Dev-Lead: Progress (in progress — not yet durable)
Plan Execution (intended)
personas/qa-lead/persona.yml:pull_request_review,issues,discussion,check_runsetenabled: false(kept, with intended-future notes;discussionrecords the fix(engine): reorder cross-provider fallback to claude → copilot → gemini #571 bridge + Epic: Persona framework — post-rollout hardening & follow-ups #1318 no-labels-API reason).mention+pull_requestremainenabled: true(the two actually wired).personas/validate-personas.py: addedcheck_surface_liveness. A surfaceenabled: truemust be served by an event in that persona'sinteraction.ymlinteraction.triggers.events[](derived, not hand-maintained). Fails closed; a persona with nointeraction.ymlis skipped with a::notice::(documented in the docstring).tests/test_validate_personas.bats: enabled-but-unserved → fail; all-served → pass; no-interaction.yml→ skip+notice; mention-via-repository_dispatchbridge → pass; one-served/one-not divergence → fail.enabled:false)issues,discussionmentionserved viarepository_dispatch:pr-review-mentionpersonas/qa-lead/interaction.ymlheader comment so it states the two files now agree (AC feat: add Copilot engine support via REVIEW_ENGINE toggle #5).Local check output (provisional — not the pushed run)
Files touched (this workspace, not yet pushed)
personas/validate-personas.py— surface-liveness invariant + docstring.personas/qa-lead/persona.yml,personas/qa-lead/interaction.yml— reconciliation.personas/{business-analyst,devops-lead,scrum-master,security-lead,solution-architect,sre-lead}/persona.yml— flipissues/discussion.tests/test_validate_personas.bats— 7 new cases.Notes
mentionsurface as served by the comment webhooks or arepository_dispatch:*bridge (so pr-review stays green), whilediscussionrequires a literaldiscussion/dispatch event it does not have. This reproduces the issue's qa-lead truth table exactly.don-petry commentedon Sep 13, 2026
Dev-Lead: Implementation Complete — PR #1790
The implementation for issue #1647 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.
66c129f4e96789edae1968504a66682befb808e4Review happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.
feat: implement issue #1647 — [qa-lead S4] Make persona.yml's declare…