Skip to content

Rename murat → qa-lead and make it @-addressable - #1279

Merged
don-petry merged 3 commits into
mainfrom
feat/qa-lead-role-rename
Jul 17, 2026
Merged

don-petry merged 3 commits into
mainfrom
feat/qa-lead-role-rename

Conversation

@don-petry

Copy link
Copy Markdown
Collaborator

Summary

Companion to petry-projects/.github#752 (the standard + schema). Applies the new "a persona is a role, not a person" rule (§1.6) and the new addressing contract (§4.1) to the worked example.

Renames via git mv (history preserved):

personas/murat/ → personas/qa-lead/
evals/murat/    → evals/qa-lead/

plus id / name / opt_out_label / canary.agent / evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it; we do not import it. @petry-projects/qa-lead says who is being addressed and why in a PR comment — the upstream cast list does not — and swapping the agent underneath now never changes how the persona is addressed. Upstream is referenced only by framework.skill: bmad-tea.

Nothing under frameworks/ is touched. Of ~40 fleet-wide murat hits, all but six are vendored and correctly keep the name. This PR changes only our layer — which is the point.

Addressing

address:
  handle: petry-projects/qa-lead
  aliases: []

The handle is an org team, never a user account. @qa-lead is a real, live GitHub account owned by someone unrelated to us — a workflow keying on the bare role would fire correctly, look healthy, and notify a stranger on every mention from a fleet of bots across public repos.

This PR declares the addressing contract but does not serve it: there is no router and no runtime yet, so nothing dispatches on the mention surface. qa-lead stays status: draft. The separation is deliberate.

Validator

Two new cross-invariants the JSON schema cannot express:

  • address.handle's team slug == id — so the router resolves a mention by stripping the org prefix, and the role name is written exactly once.
  • Handles and aliases are unique across ALL personas (case-insensitive). A collision is not cosmetic: the router resolves a handle to exactly one persona, so two claimants means a dropped mention or a double-fire. Aliases share the namespace with handles, so a rename's leftover alias cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are deliberately not checked here — they need the network, and this validator is hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification

  • bats tests/test_validate_personas.bats — 16/16 pass (10 pre-existing + 6 new: slug==id, handle collision, alias collision, case-insensitivity, distinct handles, no-address personas).
  • validate-personas.py against the §4.1 schema: OK, invariants hold.
  • python3 evals/validate-cases.py — OK, 5 skills / 46 cases, no cross-split id overlap. Eval ids renamed murat-* → qa-lead-*; only ids and descriptions changed — every input/expected is byte-identical, so the held-out set tests exactly what it did before.
  • markdownlint-cli2 clean.
  • holdout-guard passes: it fails only PRs authored by github-actions[bot] (the skill-proposer). This PR still needs CODEOWNER review over evals/ — that gate is intact.

Notes for reviewers

Merge order: .github#752 must merge first — validate-personas.py fetches the schema from .github@main, and address fails main's schema until then.

Deliberate follow-up: prompts/bmad/scrum-master.md still opens with "You are Bob" — the same §1.6 violation. That prompt drives the live initiative-planner, so changing its persona-adoption text is a behavior change that deserves its own PR and review, not a rider on a rename. The ("Murat") prose references in that file are fixed here, since they are pure prose with no runtime effect.

🤖 Generated with Claude Code

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 17, 2026 02:06
@don-petry
don-petry requested a review from a team as a code owner July 17, 2026 02:06
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@don-petry, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 84d856e6-c730-4bae-a422-845111dab982

📥 Commits

Reviewing files that changed from the base of the PR and between 6effbe7 and 68c389d.

📒 Files selected for processing (12)
  • docs/initiatives/idea-to-initiative-pipeline.md
  • evals/murat/dev/cases.jsonl
  • evals/murat/holdout/cases.jsonl
  • evals/qa-lead/dev/cases.jsonl
  • evals/qa-lead/holdout/cases.jsonl
  • personas/murat/README.md
  • personas/qa-lead/README.md
  • personas/qa-lead/persona.yml
  • personas/validate-personas.py
  • prompts/bmad/README.md
  • prompts/bmad/scrum-master.md
  • tests/test_validate_personas.bats
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/qa-lead-role-rename

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request renames the "murat" persona to "qa-lead" across documentation, manifests, and evaluation files. It also introduces validation logic in validate-personas.py to ensure that persona addressing handles are unique and that their team slugs match the persona IDs. A review comment points out a potential IndexError crash in the validator if the handle is misconfigured without a / separator, suggesting a safer parsing approach.

Comment thread personas/validate-personas.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the worked-example persona from a person-name (murat) to a role-based, org-team addressable persona (qa-lead), and extends the persona validator + tests to enforce the new addressing invariants.

Changes:

  • Add address.handle slug==id and global handle/alias uniqueness checks to personas/validate-personas.py, with new Bats coverage.
  • Rename persona/eval content from murat-* to qa-lead-* (IDs, paths, docs), and introduce the qa-lead persona README/manifest content.
  • Update BMAD orchestration and initiative pipeline docs to reference bmad-tea/qa-lead instead of the upstream person-name.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_validate_personas.bats Adds hermetic tests for addressing invariants (slug==id, uniqueness, case-insensitive collisions).
personas/validate-personas.py Implements new cross-invariants: handle slug==id and global handle/alias uniqueness.
personas/qa-lead/persona.yml Renames the persona to qa-lead and adds the address contract block.
personas/qa-lead/README.md New role-based persona documentation and addressing guidance.
personas/murat/README.md Removes the old murat persona README (rename/move).
evals/qa-lead/holdout/cases.jsonl New holdout eval cases with qa-lead-* IDs.
evals/qa-lead/dev/cases.jsonl New dev eval cases with qa-lead-* IDs.
evals/murat/holdout/cases.jsonl Removes old holdout eval cases under murat (rename/move).
evals/murat/dev/cases.jsonl Removes old dev eval cases under murat (rename/move).
prompts/bmad/scrum-master.md Updates prose references to the upstream agent skill id / qa-lead persona.
prompts/bmad/README.md Updates vendored skill documentation to reflect bmad-tea behind qa-lead.
docs/initiatives/idea-to-initiative-pipeline.md Updates initiative-planner documentation to reference bmad-tea / qa-lead.

Comment thread personas/validate-personas.py
Comment thread personas/validate-personas.py
Comment thread evals/qa-lead/holdout/cases.jsonl Outdated
don-petry added a commit to petry-projects/.github that referenced this pull request Jul 17, 2026
The qa-lead persona (PR #1279 in .github-private) introduces
`address` for @-mention routing. The canonical schema had
additionalProperties:false and no address entry, causing the
validate-personas CI check to reject the new field.

Adds an optional `address` object property with:
  - handle (required): org/team-slug — the router resolves it
    by stripping the org prefix; slug MUST equal persona id
  - aliases (optional): additional team handles (e.g. from renames)

Companion to petry-projects/.github-private#1279.
@don-petry

Copy link
Copy Markdown
Collaborator Author

Dev-Lead — waiting on PR blockers (intent: review-changes)

PR: #1279
No changes were committed, but the PR still has blocking checks or reviews (failing or cancelled checks, or changes-requested reviews). The retry cron will re-attempt automatically. Next attempt after: 2026-07-17T02:43:21Z

@don-petry

Copy link
Copy Markdown
Collaborator Author

Note

@don-petry I reviewed this PR and no code changes were needed, but it still has blocking checks or reviews (failing or cancelled checks, or changes-requested reviews), so I cannot mark it done yet. I'll re-check automatically.
Next attempt after: 2026-07-17T02:43:21Z

@don-petry
don-petry enabled auto-merge (squash) July 17, 2026 02:13
…alization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@don-petry

Copy link
Copy Markdown
Collaborator Author

Addressed the review findings in 9d1411a.

Gemini (high) + Copilot — split("/", 1)[1] IndexError; .lower() on a non-string. Both are real, and both are unreachable in CI: schema validation runs before check_invariants, and the canonical schema's pattern — now ^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$ after #752's own review tightened it — guarantees a well-formed string. Fixed anyway, because the validator accepts --schema: a test double or a stale local copy lets a malformed handle through, and this module's documented contract is "non-zero with a diagnostic", not a traceback.

  • New handle_slug() validates org/team-slug and returns the slug, failing cleanly on a non-string or slash-less value.
  • check_invariants now also validates address is a mapping and aliases is a list of well-formed handles. Alias shape is checked there, not in check_handles_unique — since that runs only after every manifest has cleared check_invariants, it can now treat handles and aliases as well-formed by construction. Noted in its docstring.

Copilot — capitalization. Correct; my rename sed introduced it. Only that one instance followed a period (the other three follow semicolons and were already right). Description text only — no input/expected touched, so the held-out set still tests exactly what it did before.

Verification

  • bats tests/test_validate_personas.bats — 20/20 (16 + 4 new: bare handle, non-string handle, malformed alias, non-list aliases; each asserts a clean diagnostic and the absence of a traceback).
  • validate-personas.py against the live merged schema on .github@main: OK, invariants hold.
  • evals/validate-cases.py: OK, 5 skills / 46 cases.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 17, 2026
@don-petry
don-petry disabled auto-merge July 17, 2026 02:31
donpetry-bot
donpetry-bot previously approved these changes Jul 17, 2026

@donpetry-bot donpetry-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — APPROVED ✓

Risk: MEDIUM
Reviewed commit: 3748c6b676bcc3a8576369055a6106debb911a77
Cascade: triage → deep (triage: haiku 4.5 → deep: opus 4.8 + duck: o4-mini → audit: fable 5)

Summary

Renames the murat worked-example persona to the role-based, @-addressable qa-lead and adds two hermetic cross-invariants (handle slug==id, handle/alias global uniqueness) to validate-personas.py with 6 new passing bats tests. The IndexError/AttributeError flagged by triage/gemini/copilot in the new validator code is an unreachable false positive: the .github@main schema pins handle to a string pattern requiring 'petry-projects/' and aliases to a string array, and fail() sys.exit(1)s on the first schema violation before check_invariants and check_handles_unique ever run, so every value reaching split('/')[1]/.lower() is a well-formed string. No secrets, no downstream consumers (none), and all CI gates (validate-personas, gitleaks, CodeQL, SonarCloud, agent-shield, holdout/test-deletion guards, 16/16 bats) are green.

Findings

  • MINOR: check_invariants does address['handle'].split('/', 1)[1] with no guard. Flagged HIGH by gemini and copilot as an IndexError crash path, but it is unreachable: the persona schema pins handle to pattern ^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$ (mandates a '/'), and main() runs jsonschema iter_errors -> fail() -> sys.exit(1) before check_invariants, so any handle reaching the split contains a '/'. Even if reachable, the crash is a non-zero exit of a hermetic CI lint that still rejects the bad manifest — no bypass. Optional defensive '/'-check would only improve the diagnostic message. (personas/validate-personas.py:125)
  • MINOR: check_handles_unique calls handle.lower() and iterates address.get('aliases', []) without type-guarding (copilot: AttributeError on non-string handle / non-list aliases). Also unreachable: check_handles_unique runs only after the per-manifest loop, and fail()/sys.exit(1) fires on any schema violation first; the schema types handle as string and aliases as array-of-string, so loaded[] only ever holds schema-valid manifests. Same non-blocking diagnostic-quality nit. (personas/validate-personas.py:151)
  • INFO: Copilot grammar nit: an eval case description starts mid-sentence with 'the QA Lead' after a period; should be 'The QA Lead'. Description text only — no effect on eval scoring (input/expected are byte-identical to the murat originals; holdout-guard and test-deletion-guard pass). (evals/qa-lead/holdout/cases.jsonl:3)
  • INFO: Merge-order dependency noted by author: .github#752 (schema + standard) must merge before this PR, since validate-personas.py fetches the address schema from .github@main. The schema at main already carries the address block (verified during review), so the dependency appears satisfied. mergeStateStatus BLOCKED reflects REVIEW_REQUIRED only, not a failing check.

Reviewed by the PR-review cascade (triage: haiku 4.5 → deep: opus 4.8 + duck: o4-mini → audit: fable 5). Reply if you need a human review.

@don-petry

Copy link
Copy Markdown
Collaborator Author

Dev-Lead — review-changes (applied)

Changes committed and pushed.

@don-petry
don-petry dismissed stale reviews from donpetry-bot and coderabbitai[bot] via 68c389d July 17, 2026 02:35
@don-petry
don-petry enabled auto-merge (squash) July 17, 2026 02:35
@don-petry
don-petry disabled auto-merge July 17, 2026 02:36
@sonarqubecloud

Copy link
Copy Markdown

@don-petry

Copy link
Copy Markdown
Collaborator Author

Dev-Lead — waiting on PR blockers (intent: review-changes)

PR: #1279
No changes were committed, but the PR still has blocking checks or reviews (failing or cancelled checks, or changes-requested reviews). The retry cron will re-attempt automatically. Next attempt after: 2026-07-17T03:11:00Z

@don-petry

Copy link
Copy Markdown
Collaborator Author

Note

@don-petry I reviewed this PR and no code changes were needed, but it still has blocking checks or reviews (failing or cancelled checks, or changes-requested reviews), so I cannot mark it done yet. I'll re-check automatically.
Next attempt after: 2026-07-17T03:11:00Z

@don-petry
don-petry enabled auto-merge (squash) July 17, 2026 02:41

@donpetry-bot donpetry-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — APPROVED ✓

Risk: MEDIUM
Reviewed commit: 68c389db9ffaffed9a33b77fdea93204d378bef3
Review mode: triage-approved (single reviewer)

Summary

Renames the worked-example persona murat → qa-lead (role, not person) per persona-standards §1.6, adds an org-team addressing contract (§4.1), and extends validate-personas.py with two cross-file invariants (handle slug == id; fleet-wide handle/alias uniqueness, case-insensitive) plus 11 new hermetic bats tests. Companion to petry-projects/.github#752. All prior review findings (Gemini high IndexError, Copilot type-safety and capitalization) are verifiably fixed at head.

Linked issue analysis

No linked issues (closingIssuesReferences is empty). The PR is a companion to petry-projects/.github#752 (the standard + schema), which is already merged — the validate-personas CI job passes against the live canonical schema.

Findings

Confirmation review of the triage assessment — the triage tier's low-risk clearance holds, with one nuance:

  • Prior findings all resolved at head. Gemini's high-priority finding (split("/", 1)[1] IndexError on a slash-less handle) and Copilot's two findings (.lower() on non-string; capitalization in an eval description) were fixed in 9d1411a: the new handle_slug() helper validates isinstance(str) and "/" in value before splitting and emits a fail() diagnostic, check_invariants validates address mapping/alias shapes, and 4 new bats tests assert diagnostics without tracebacks. bats: green in CI.
  • One review thread is technically unresolved (Gemini's IndexError thread) but it is substantively addressed: the fix is in the head commit with test coverage, and the author replied in-thread with specifics. Gemini threads are not auto-resolvable by the fix author; not treating this as blocking.
  • Validator logic is sound. check_handles_unique runs only after every manifest clears check_invariants, aliases share the namespace with handles, and collisions are case-insensitive — matching how GitHub resolves team mentions. Ordering invariant is documented in the docstring.
  • Rename hygiene is good. persona.yml moved via git mv (62% similarity, history preserved); eval case ids renamed with content otherwise unchanged (holdout-guard green); frameworks/ untouched as claimed — vendored upstream names correctly retained; no thin caller stubs modified.
  • Addressing contract declared, not served — no router/runtime exists, persona stays status: draft, address.handle is an org team (never a user account, avoiding the real @qa-lead GitHub user). Live team properties are explicitly deferred to a networked CI job; validator stays hermetic. Sensible separation.
  • Secret scan: the run_secret_scanning MCP tool is not available in this environment; the gitleaks CI check passed and the diff contains no credential-like content.

Risk is MEDIUM (non-trivial validator logic + cross-module rename), not HIGH: no auth/secrets/migrations, no workflow files touched, no standards violations.

CI status

All checks green for head 68c389d: shellcheck, ShellCheck, actionlint, bats, unit-tests, validate-personas, holdout-guard, gitleaks, CodeQL (actions + python), Agent Security Scan, agent-shield, SonarCloud quality gate, caller-stub-freeze, template-drift, gh-aw-compile, CodeRabbit. A few CANCELLED entries (review/review, dev-lead/dispatch, dev-lead/ci-relay) are concurrency-superseded duplicate runs — each has a later SUCCESS (ci-relay intentionally skipped via [skip ci-relay]). Dependency-audit jobs skipped (no matching ecosystems changed).


Reviewed automatically by the PR-review agent (single-reviewer mode: fable 5). Reply if you need a human review.

@don-petry
don-petry merged commit bb71667 into main Jul 17, 2026
42 of 46 checks passed
@don-petry
don-petry deleted the feat/qa-lead-role-rename branch July 17, 2026 02:46
don-petry added a commit that referenced this pull request Aug 2, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 3, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 3, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 3, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 7, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 8, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 8, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 8, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 8, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Aug 18, 2026
* feat(personas): rename murat -> qa-lead and make it @-addressable

Companion to petry-projects/.github#<A> (the standard + schema). Applies the
new "a persona is a role, not a person" rule (§1.6) and the new addressing
contract (§4.1) to the worked example.

Renames (git mv, history preserved):
  personas/murat/ -> personas/qa-lead/
  evals/murat/    -> evals/qa-lead/
plus id/name/opt_out_label/canary.agent/evals.path and the eval case ids.

The role, not the cast list

The vendored agent this persona wraps has a person-name upstream. It keeps it;
we do not import it. `@petry-projects/qa-lead` says who is being addressed and
why in a PR comment — the upstream cast list does not — and swapping the agent
underneath now never changes how the persona is addressed. Upstream is
referenced only by `framework.skill: bmad-tea`.

Nothing under frameworks/ is touched. Of ~40 fleet-wide "murat" hits, all but
six are vendored and correctly keep the name; this PR changes only our layer.

Addressing

  address:
    handle: petry-projects/qa-lead
    aliases: []

The handle is an org TEAM, never a user account. `@qa-lead` is a real, live
GitHub account owned by someone unrelated to us — a workflow keying on the
bare role would fire correctly, look healthy, and notify a stranger on every
mention from a fleet of bots across public repos.

Note this PR declares the addressing contract but does not serve it: there is
no router and no runtime yet, so nothing dispatches on the mention surface.
qa-lead stays `status: draft`. That separation is deliberate.

Validator (personas/validate-personas.py)

Two new cross-invariants the JSON schema cannot express:
  * address.handle's team slug == id — so the router resolves a mention by
    stripping the org prefix, and the role name is written exactly once.
  * handles and aliases are unique across ALL personas (case-insensitive).
    A collision is not cosmetic: the router resolves a handle to exactly one
    persona, so two claimants means a dropped mention or a double-fire.
    Aliases share the namespace with handles, so a rename's leftover alias
    cannot shadow a live role.

Live team properties (exists / privacy: closed / notifications_disabled) are
deliberately NOT checked here — they need the network, and this validator is
hermetic with a hermetic bats suite. Those belong to a CI step and the §7 DoD.

Verification
- bats tests/test_validate_personas.bats: 16/16 pass (10 pre-existing + 6 new
  covering slug==id, handle collision, alias collision, case-insensitivity,
  distinct handles, and no-address personas).
- validate-personas.py against the §4.1 schema: OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases, no cross-split id
  overlap (eval ids renamed murat-* -> qa-lead-*; only ids/descriptions
  changed, every `input`/`expected` is byte-identical, so the held-out set
  tests exactly what it did before).
- markdownlint-cli2 clean.
- holdout-guard passes: it fails only PRs authored by github-actions[bot]
  (the skill-proposer); this PR still needs CODEOWNER review over evals/.

Merge order: .github#<A> must merge first — validate-personas.py fetches the
schema from .github@main, and `address` fails main's schema until then.

Follow-up, deliberately not here: prompts/bmad/scrum-master.md still opens with
"You are Bob". Same violation, but that prompt drives the live
initiative-planner, so changing its persona-adoption text is a behavior change
that deserves its own PR and review — not a rider on a rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(personas): address review — handle-shape diagnostics + case capitalization

Gemini (high) and Copilot both flagged that `address["handle"].split("/", 1)[1]`
raises IndexError on a handle with no '/', crashing the validator instead of
emitting a diagnostic. Copilot additionally flagged `.lower()` on a non-string
handle/alias.

Both are unreachable in CI: schema validation runs before check_invariants, and
the canonical schema's pattern (now `^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$`
after .github#752's own review) guarantees a well-formed string. They are worth
fixing anyway because the validator accepts `--schema` — a test double or a
stale local copy lets a malformed handle through, and this module's documented
contract is "non-zero with a diagnostic", not a traceback.

- New `handle_slug()` validates 'org/team-slug' and returns the slug, failing
  with a diagnostic on a non-string or slash-less value.
- check_invariants now also validates `address` is a mapping and `aliases` is a
  list of well-formed handles. Alias shape is checked THERE, not in
  check_handles_unique, so that by the time the latter runs — after every
  manifest has cleared check_invariants — it can treat handles and aliases as
  well-formed strings by construction. Documented in its docstring.

Copilot also caught a capitalization slip my rename sed introduced:
"...no behavior change. the QA Lead must NOT manufacture risk" -> "The QA Lead".
Only that one instance followed a period; the other three follow semicolons and
are correct. Description text only — no `input`/`expected` touched.

Verification:
- bats tests/test_validate_personas.bats: 20/20 (16 + 4 new covering bare
  handle, non-string handle, malformed alias, non-list aliases — each asserting
  a clean diagnostic AND the absence of a traceback).
- validate-personas.py against the LIVE merged schema on .github@main (with the
  tightened petry-projects-anchored pattern): OK, invariants hold.
- python3 evals/validate-cases.py: OK, 5 skills / 46 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants