Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 98 additions & 6 deletions standards/persona-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
Standard for defining, onboarding, and rolling out a new **agentic persona**
across the `petry-projects` fleet.

A persona is a named agent role — Dev-Lead, PR-Reviewer, Bob (Scrum Master),
Mary (Analyst), Murat (Test Architect) — that helps the org **design, build,
test, deliver, and operate** its projects. This standard turns the pattern that
A persona is an agent **role** — Dev Lead, PR Review, Scrum Master, Business
Analyst, QA Lead — that helps the org **design, build, test, deliver, and
operate** its projects. Roles, never person-names: see principle 6. This
standard turns the pattern that
is already implicit across those personas into an explicit, repeatable
**Persona Definition of Done**: a manifest, a trigger checklist, a file layout,
an eval gate, and a canary onboarding step.
Expand Down Expand Up @@ -52,6 +53,28 @@ an eval gate, and a canary onboarding step.
discipline in `.github-private` `evals/` (dev/holdout split, `holdout-guard.yml`)
is a promotion gate, not a nicety.

6. **A persona is a role, not a person.** `id` and `name` name the **role** —
`qa-lead` / "QA Lead", never `murat` / "Murat". This is the naming half of
principle 2: an upstream framework may give its agent a person-name, and that
is upstream's business. We reference upstream agents **only** by their
technical skill id (`definition.layers[].framework.skill`, e.g. `bmad-tea`);
we never mirror the person-name into our namespace. The reasons are practical,
not stylistic:

- **A role is legible.** `@petry-projects/qa-lead` tells a reader in a PR
comment who is being addressed and why. `@petry-projects/murat` requires
knowing the BMAD cast list.
- **A role is stable.** Swapping the framework agent behind a persona, or
replacing it with a first-party layer, must not change how it is addressed.
Binding our identity to upstream's cast means their rename is our
fleet-wide migration.
- **A role is one namespace.** `dev-lead` and `pr-review` were already
role-named; only the framework-backed personas drifted. One rule, no
exceptions.

Roles come from the org's own vocabulary: `dev-lead`, `qa-lead`,
`scrum-master`, `business-analyst`, `product-manager`, `pr-review`.

---

## 2. Where a persona lives
Expand Down Expand Up @@ -141,7 +164,7 @@ on**. Fill one row per surface; anything unlisted falls back to `default_mode`.
| `discussion` | `created`, `labeled`, `commented` | Cannot run the agent inline — **must** bridge via `repository_dispatch`/`workflow_dispatch` (see `ci-standards.md`, issue #571) |
| `check_run` | `completed` | CI-failure follow-up |
| `schedule` | cron | Proactive / sweep work |
| `mention` | `@<bot>` in a comment | Reviewer-assignment counts here too |
| `mention` | `@<handle>` in a comment | How a human addresses the persona directly — see §4.1. Reviewer-assignment counts here too |

**Rules:**

Expand All @@ -164,6 +187,69 @@ Label gates and trust checks are defined operationally in `ci-standards.md`
(§ agent-trigger labels) and must reuse the existing labels rather than mint
near-duplicates.

### 4.1 Addressing — `@`-mentioning a persona

A persona that enables the `mention` surface MUST declare an `address` block
(schema-enforced). It is addressed by its **role**, per principle 6:

```yaml
address:
handle: petry-projects/qa-lead # an org TEAM — mentioned as @petry-projects/qa-lead
aliases: [] # additional handles that route here (renames)
```

**The handle MUST be an org team, never a user account.** This is not a style
preference — it is the whole reason the block exists:

> GitHub's user namespace is **global and first-come**. `@qa-lead`, `@dev-lead`,
> `@scrum-master`, `@business-analyst` and `@murat` are **all real, live GitHub
> accounts today** — none of them ours. A workflow keying on
> `contains(body, '@qa-lead')` would fire correctly and look perfectly healthy
> while every mention publicly tagged a stranger and sent them a notification,
> from a fleet of bots, across public repos. The failure is silent on our side
> and only visible on theirs.

An org team slug lives in the org's namespace and cannot collide. It also buys
two things a bare convention cannot:

- **Autocomplete** — typing `@petry-projects/` in any GitHub comment box lists
every persona. The addressing scheme *is* the discovery mechanism.
- **A real link** — the mention renders as a resolvable team, not grey text.

**Rules:**

1. **`handle` is `org/team-slug`, and the slug MUST equal `id`.** Routing is
then a prefix-strip, and "register once" holds: the role name is written in
exactly one place.
2. **The team MUST be `privacy: closed`.** Secret teams cannot be mentioned at
all; closed teams are mentionable by org members.
3. **The team MUST set `notification_setting: notifications_disabled`.** The
handle exists to route a webhook, not to page humans. Membership should be
empty or bot-only.
4. **Handles and aliases MUST be unique across all personas.** Cross-file, so
JSON Schema cannot see it — enforced by `validate-personas.py`.
5. **A rename keeps the old handle as an `alias`.** Roles outlive their spelling;
in-flight threads must not break.

The **live** properties (rules 2 and 3 — does the team exist, is it closed, are
notifications off?) need the network, and `validate-personas.py` is hermetic
with a hermetic test suite. They are verified by a separate CI step and are a
line in the §7 Definition of Done — deliberately **not** in the validator, whose
hermeticity is worth more than the check.

Mention handling is centralised: one router resolves the handle, consults that
persona's own trigger matrix, applies the trust floor and `opt_out_label`, and
dispatches. Personas do **not** each ship a mention workflow — that is the
per-agent drift the manifest exists to prevent.

> **Recursion is the hazard here.** Agent comments posted via a PAT re-trigger
> workflows (unlike `GITHUB_TOKEN`), and `.github-private#860` burned 1,481
> identical acks in 4.5 hours from a *single* self-loop. With N mutually
> addressable personas the cycles are combinatorial, not self-loops: `qa-lead`
> answering a thread that mentions `dev-lead` is enough. Every router MUST
> exclude on two independent axes — the **bot actor** and an
> **agent-comment marker** — as `pr-review-mention.yml` already does.

---

## 5. Trust, permissions, and safety
Expand Down Expand Up @@ -227,9 +313,15 @@ A persona is "done" (ready for `stable`) when all of the following are true:
- [ ] `definition.layers[]` names every implementing layer; framework layers pin
a `vendor_pin` matching the framework's `VENDOR.md`, and any local override
sets `upstream_candidate` truthfully.
- [ ] `id` and `name` are the **role**, not a person (§1.6); upstream agents are
referenced only by `framework.skill`.
- [ ] `triggers` fills one row per surface the persona acts on; `default_mode`
is `advisory`/`off`; every `write` surface has a `gate_label`; an
`opt_out_label` is defined.
- [ ] If the `mention` surface is enabled: `address.handle` is an **org team**
whose slug equals `id`; the team exists, is `privacy: closed`, and sets
`notification_setting: notifications_disabled`; the handle and every alias
are unique fleet-wide (§4.1).
- [ ] `trust.author_association_floor` is set; write surfaces gate on it.
- [ ] If it ships a reusable: caller stub grants a **superset** of
`runtime.permissions`, carries the `SOURCE OF TRUTH` header, and pins a
Expand All @@ -242,8 +334,8 @@ A persona is "done" (ready for `stable`) when all of the following are true:
- [ ] The persona has soaked through `next → ring0 → ring1` before `stable`.

Copy [`standards/personas/TEMPLATE/`](personas/TEMPLATE/) to start; the worked
example is **Murat** (`personas/murat/` in `.github-private`), which wraps the
vendored `bmad-test-architecture` Test Architect.
example is **QA Lead** (`personas/qa-lead/` in `.github-private`), which wraps
the vendored `bmad-test-architecture` agent (`framework.skill: bmad-tea`).

---

Expand Down
26 changes: 19 additions & 7 deletions standards/personas/TEMPLATE/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ inside `personas/<id>/`.
## Onboarding order

1. **Copy & rename.** `cp standards/personas/TEMPLATE/persona.yml .github-private/personas/<id>/persona.yml` (and add a short `README.md`).
Pick `<id>` as a **role** — `qa-lead`, not a person name, even if the agent
you are wrapping has one upstream ([§1.6](https://github.com/petry-projects/.github/blob/main/standards/persona-standards.md)).
2. **Pick your definition layer(s)** in `persona.yml` `definition.layers[]`, each
`path` pointing at its canonical home above. Prefer wrapping a vendored
framework agent where a good upstream exists (pin `vendor_pin` to its
Expand All @@ -45,21 +47,31 @@ inside `personas/<id>/`.
Every `write` surface needs a `gate_label`. Define an `opt_out_label`.
4. **Set the trust floor** (`[OWNER, MEMBER, COLLABORATOR]` unless you have a
reason to differ).
5. **Add evals.** Copy the `evals/` starter into `.github-private` `evals/<id>/`:
5. **If the `mention` surface is enabled, fill `address`** and create the org
team `petry-projects/<id>` — `privacy: closed`,
`notification_setting: notifications_disabled`, membership empty or bot-only.
The handle **must** be a team; a bare `@<role>` is a real stranger's account
([§4.1](https://github.com/petry-projects/.github/blob/main/standards/persona-standards.md)).
6. **Add evals.** Copy the `evals/` starter into `.github-private` `evals/<id>/`:
≥ `min_cases` held-out cases under `evals/<id>/holdout/` and proposer-visible
cases under `evals/<id>/dev/`. See `.github-private` `evals/README.md`.
6. **Register the canary entry** — one `agents.<id>` block in
7. **Register the canary entry** — one `agents.<id>` block in
`standards/canary-rings.json` (the *only* place rings are written). Point
`persona.yml` `canary.agent` at it.
7. **Validate & scan.** Manifest validates against the schema; AgentShield passes;
8. **Validate & scan.** Manifest validates against the schema; AgentShield passes;
no immutable file touched.
8. **Roll out** `next → ring0 → ring1 → stable`, eval gate green before `stable`.
9. **Roll out** `next → ring0 → ring1 → stable`, eval gate green before `stable`.

The full gate is the **Definition of Done** checklist in
[`persona-standards.md` §7](https://github.com/petry-projects/.github/blob/main/standards/persona-standards.md).

## Worked example

**Murat** (`.github-private/personas/murat/`) wraps the vendored
`bmad-test-architecture` Test Architect — the reference for the
*wrap-a-vendored-agent*, advisory-everywhere path.
**QA Lead** (`.github-private/personas/qa-lead/`) wraps the vendored
`bmad-test-architecture` agent (`framework.skill: bmad-tea`) — the reference for
the *wrap-a-vendored-agent*, advisory-everywhere path.

Note the naming: the persona is `qa-lead`, the **role**, even though the
upstream framework gives that agent a person-name. Roles are legible in a PR
comment, survive swapping the agent underneath, and keep one namespace across
the fleet. See [§1.6](https://github.com/petry-projects/.github/blob/main/standards/persona-standards.md).
29 changes: 26 additions & 3 deletions standards/personas/TEMPLATE/persona.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
schema_version: 1

# Stable kebab-case id. MUST equal this directory's name AND the agents.<key>
# in canary-rings.json AND the channel-tag agent name.
# in canary-rings.json AND the channel-tag agent name AND the team slug in
# address.handle below.
# (lowercase kebab-case — the schema enforces ^[a-z0-9]+(-[a-z0-9]+)*$)
#
# MUST be a ROLE, never a person name (§1.6): 'qa-lead', not 'murat'. If this
# persona wraps an upstream framework agent that has a person-name, that name
# stays upstream — reference it only via framework.skill below.
id: change-me

name: CHANGE-ME # display name, e.g. "Murat"
name: CHANGE-ME # role display name, e.g. "QA Lead" — NOT a person
title: CHANGE-ME # role title, e.g. "Master Test Architect & Quality Advisor"
summary: >-
One sentence: what this persona is for and where it helps
Expand Down Expand Up @@ -60,6 +65,24 @@ definition:
# - kind: gh-aw
# path: .github/workflows/CHANGE-ME.md

# --- How humans address this persona from a comment (§4.1) ----------------
# REQUIRED if the 'mention' surface below is enabled (schema-enforced).
#
# MUST be an org TEAM handle, never a user account. GitHub's user namespace is
# global and first-come: '@qa-lead', '@dev-lead' and '@scrum-master' are all
# real accounts owned by strangers. Keying a workflow on a bare '@<role>' fires
# correctly, looks healthy, and notifies an unrelated person on every mention.
# An org team slug cannot collide — and it autocompletes when you type
# '@petry-projects/' in any comment box.
#
# The team slug MUST equal `id`. Create the team as privacy: closed (secret
# teams cannot be mentioned) with notification_setting: notifications_disabled
# (the handle routes a webhook; it should not page humans). CI verifies those
# live properties — the schema cannot.
address:
handle: petry-projects/change-me # slug MUST equal `id` above
aliases: [] # old handles kept routing after a rename

# --- Capabilities the persona can invoke (advisory index) -----------------
skills: []
# To add skills, replace the empty array above with a block sequence:
Expand Down Expand Up @@ -92,7 +115,7 @@ triggers:
events: [created]
enabled: true
mode: advisory
notes: "e.g. @<bot> weigh in"
notes: "e.g. @petry-projects/<id> weigh in — requires the address block above"

# discussion surfaces cannot run the agent inline — declare the bridge:
# - surface: discussion
Expand Down
46 changes: 43 additions & 3 deletions standards/personas/persona.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@
{
"if": {"properties": {"status": {"const": "stable"}}, "required": ["status"]},
"then": {"required": ["evals"], "description": "A persona at status 'stable' MUST carry an evals block (the held-out gate). See persona-standards.md §1."}
},
{
"if": {
"required": ["triggers"],
"properties": {
"triggers": {
"required": ["surfaces"],
"properties": {
"surfaces": {
"contains": {
"required": ["surface", "enabled"],
"properties": {"surface": {"const": "mention"}, "enabled": {"const": true}}
}
}
}
}
}
},
"then": {"required": ["address"], "description": "A persona that enables the 'mention' surface MUST declare how it is addressed. See persona-standards.md §4.1."}
}
],
"properties": {
Expand All @@ -21,12 +40,12 @@
"id": {
"type": "string",
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
"description": "Stable kebab-case identifier. MUST equal the persona directory name AND the channel-tag agent name used in canary-rings.json (e.g. 'murat', 'dev-lead')."
"description": "Stable kebab-case identifier. MUST be a ROLE, never a person name — 'qa-lead', not 'murat' (persona-standards.md §1.6). MUST equal the persona directory name, the addressing team slug in `address.handle`, AND the channel-tag agent name used in canary-rings.json (e.g. 'qa-lead', 'dev-lead')."
},
"name": {
"type": "string",
"minLength": 1,
"description": "Human display name (e.g. 'Murat'). For framework-backed personas this MUST match the locked framework identity."
"description": "Human display name for the ROLE (e.g. 'QA Lead', 'Dev Lead'). MUST NOT be a person name, even when this persona wraps an upstream framework agent that has one. Upstream agents are referenced ONLY by their technical skill id (definition.layers[].framework.skill, e.g. 'bmad-tea'); their persona names are upstream's business and are not mirrored here. See persona-standards.md §1.6."
},
"title": {
"type": "string",
Expand Down Expand Up @@ -81,7 +100,7 @@
"name": {"type": "string", "description": "Framework directory under frameworks/ (e.g. 'bmad-test-architecture')."},
"upstream_repo": {"type": "string", "description": "Upstream source repo (e.g. 'bmad-code-org/bmad-method-test-architecture-enterprise')."},
"vendor_pin": {"type": "string", "description": "Vendored version/tag, MUST match the framework's VENDOR.md (e.g. 'v1.19.0')."},
"skill": {"type": "string", "description": "The framework skill/agent id backing this persona (e.g. 'bmad-tea')."}
"skill": {"type": "string", "description": "The framework skill/agent id backing this persona (e.g. 'bmad-tea'). This is the ONLY handle we keep on an upstream agent: a technical id, never the persona name upstream may give it. If you need to know what upstream calls it, read upstream."}
}
},
"local_overrides": {
Expand All @@ -106,6 +125,27 @@
}
}
},
"address": {
"type": "object",
"additionalProperties": false,
"description": "How a human addresses this persona from a comment (`@<handle>`). REQUIRED when the 'mention' surface is enabled. See persona-standards.md §4.1.",
"required": ["handle"],
"properties": {
"handle": {
"type": "string",
"pattern": "^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$",
"description": "The addressing handle in 'org/team-slug' form, mentioned as '@org/team-slug'. MUST be an org TEAM, never a user account: GitHub's user namespace is global and first-come, so a bare '@qa-lead' resolves to an unrelated real account and notifies a stranger on every mention (verified: qa-lead, dev-lead, scrum-master are all live accounts). An org team slug cannot collide. The slug MUST equal `id`, so routing is a prefix-strip. Team must be privacy 'closed' (secret teams are unmentionable) with notification_setting 'notifications_disabled'; those are live properties CI verifies, not shape this schema can check."
},
"aliases": {
"type": "array",
"items": {
"type": "string",
"pattern": "^petry-projects/[a-z0-9]+(-[a-z0-9]+)*$"
},
"description": "Additional handles that route here — the migration path when a role is renamed. Same org/team-slug rules as `handle`. Every alias must be unique across ALL personas' handles and aliases (cross-file, enforced by validate-personas.py)."
}
}
},
"skills": {
"type": "array",
"description": "Capabilities the persona can invoke, consumed by path (vendor-neutral). Advisory index for humans; the authoritative wiring is inside the definition layers.",
Expand Down
Loading