Repository navigation
The persona runtime — dispatch side of the mention framework #1293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c2daf58
feat(personas): the persona runtime — dispatch side of the mention fr…
don-petry 0786311
fix(reviews): address review comments [skip ci-relay]
donpetry-bot d93e5fb
fix(personas): make the recursion marker MECHANICALLY enforced (review)
don-petry b6f12fa
fix(reviews): address review comments [skip ci-relay]
donpetry-bot e0668cb
fix(personas): restore the CRLF test; pin the COMMENT_URL contract
don-petry File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| # Persona Runtime — Reusable (dispatch side of the mention framework). | ||
| # This file is the source of truth. Caller: .github/workflows/persona-runner.yml. | ||
| # Standard: https://github.com/petry-projects/.github/blob/main/standards/persona-standards.md (§4.1) | ||
| # | ||
| # The router (petry-projects/.github persona-mention-reusable.yml) resolves an | ||
| # `@petry-projects/<role>` mention and fires a `persona-mention` | ||
| # repository_dispatch at this repo. This workflow receives it and runs the | ||
| # addressed persona's advisory prompt (prompts/<id>/advisory.md), posting exactly | ||
| # one advisory comment that carries the '<!-- persona:<id> -->' recursion marker. | ||
| # | ||
| # ONE runner for EVERY persona — manifest/convention driven, not one workflow per | ||
| # persona (§4.1). Advisory only: this runtime is granted no write beyond posting | ||
| # a comment, and the prompt forbids anything else. | ||
| # | ||
| # Requires: | ||
| # CLAUDE_CODE_OAUTH_TOKEN — org secret, the engine credential | ||
| # GH_PAT_WORKFLOWS — org PAT: read the target item cross-repo, post the | ||
| # comment. A PAT (not github.token) because the item | ||
| # lives in a DIFFERENT repo than this workflow. | ||
| name: Persona Runtime — Reusable | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| persona: | ||
| required: true | ||
| type: string | ||
| source_repo: | ||
| required: true | ||
| type: string | ||
| item_number: | ||
| required: false | ||
| type: string | ||
| default: '' | ||
| comment_url: | ||
| required: false | ||
| type: string | ||
| default: '' | ||
| requested_by: | ||
| required: false | ||
| type: string | ||
| default: '' | ||
| mode: | ||
| required: false | ||
| type: string | ||
| default: 'advisory' | ||
| secrets: | ||
| CLAUDE_CODE_OAUTH_TOKEN: | ||
| required: true | ||
| GH_PAT_WORKFLOWS: | ||
| required: true | ||
|
|
||
| permissions: {} # nothing from github.token; all cross-repo work uses the PAT | ||
|
|
||
| concurrency: | ||
| # One run per (persona, item). A second mention while one is in flight is | ||
| # dropped rather than stacked — the advisory would be redundant. | ||
| group: persona-runner-${{ inputs.persona }}-${{ inputs.source_repo }}-${{ inputs.item_number }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| run: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| permissions: | ||
| contents: read # checkout of THIS repo's tooling + prompts; all cross-repo | ||
| # reads and the comment-post use GH_PAT_WORKFLOWS, not this. | ||
| steps: | ||
| - name: Checkout persona tooling + prompts | ||
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||
| with: | ||
| fetch-depth: 1 | ||
| persist-credentials: false | ||
|
|
||
| - name: Resolve the persona's advisory prompt | ||
| id: resolve | ||
| env: | ||
| PERSONA: ${{ inputs.persona }} | ||
| run: | | ||
| set -euo pipefail | ||
| # shellcheck source=scripts/lib/persona-runner.sh | ||
| source scripts/lib/persona-runner.sh | ||
|
|
||
| # This runner is a SEPARATE trust boundary from the router: a | ||
| # repository_dispatch can be sent by anything holding a token, so the | ||
| # payload is re-validated here rather than trusted. A malformed id must | ||
| # never reach a path interpolation. | ||
| if ! prompt_path="$(pr_require_advisory "$PERSONA" .)"; then | ||
| echo "::notice::persona '${PERSONA}' has no advisory runtime — nothing to do" | ||
| echo "skip=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||
| echo "prompt_path=${prompt_path}" >> "$GITHUB_OUTPUT" | ||
| echo "marker=$(pr_agent_marker "$PERSONA")" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Install Claude Code | ||
| if: steps.resolve.outputs.skip == 'false' | ||
| env: | ||
| CLAUDE_CODE_VERSION: ${{ vars.CLAUDE_CODE_VERSION || 'latest' }} | ||
| run: | | ||
| set -euo pipefail | ||
| mkdir -p "$HOME/.npm-global" | ||
| npm config set prefix "$HOME/.npm-global" | ||
| echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH" | ||
| npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" | ||
|
|
||
| - name: Run the persona advisory | ||
| if: steps.resolve.outputs.skip == 'false' | ||
| env: | ||
| CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | ||
| # The PAT posts the comment and reads the cross-repo item. Exported as | ||
| # GH_TOKEN for the gh calls the agent makes from inside the prompt. | ||
| GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} | ||
| PERSONA: ${{ inputs.persona }} | ||
| SOURCE_REPO: ${{ inputs.source_repo }} | ||
| ITEM_NUMBER: ${{ inputs.item_number }} | ||
| COMMENT_URL: ${{ inputs.comment_url }} | ||
| REQUESTED_BY: ${{ inputs.requested_by }} | ||
| AGENT_MARKER: ${{ steps.resolve.outputs.marker }} | ||
| PROMPT_PATH: ${{ steps.resolve.outputs.prompt_path }} | ||
| run: | | ||
| set -euo pipefail | ||
| export PATH="$HOME/.npm-global/bin:$PATH" | ||
|
|
||
| # The advisory prompt is the persona's instructions; the header pins the | ||
| # concrete item so the agent does not have to be told twice. Fields go in | ||
| # via env (already exported) — never interpolated into the prompt text, | ||
| # which the agent could be steered by. | ||
| { | ||
| echo "You are running headless for persona '${PERSONA}'. The work item is" | ||
| echo "${SOURCE_REPO}#${ITEM_NUMBER} (${COMMENT_URL}); ${REQUESTED_BY} summoned you." | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| echo "Follow your instructions exactly. Post ONE comment; its first line" | ||
| echo "MUST be exactly: ${AGENT_MARKER}" | ||
| echo | ||
| cat "$PROMPT_PATH" | ||
| } > /tmp/persona-prompt.txt | ||
|
|
||
| _out=$(mktemp); _rc=0 | ||
| claude --print \ | ||
| --permission-mode bypassPermissions \ | ||
| --allowedTools Bash \ | ||
| --fallback-model opus \ | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| -p "$(cat /tmp/persona-prompt.txt)" 2>&1 | tee "$_out" || _rc=${PIPESTATUS[0]} | ||
|
|
||
| # Rate-limit is a soft skip, matching ci-failure-analyst: do not fail the | ||
| # run (and do not retry-storm), just leave the mention un-answered. | ||
| if [ "$_rc" -ne 0 ] && \ | ||
| grep -qiE "hit your.*(session )?limit|rate.?limit|usage limit|quota exceeded|too many requests" "$_out"; then | ||
| echo "::notice::engine rate-limited — leaving ${SOURCE_REPO}#${ITEM_NUMBER} un-answered (a future mention retries)" | ||
| rm -f "$_out"; exit 0 | ||
| fi | ||
| rm -f "$_out" | ||
| exit "$_rc" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Persona Runtime — caller (receives the router's dispatch). | ||
| # SOURCE OF TRUTH: petry-projects/.github-private/.github/workflows/persona-runner-reusable.yml | ||
| # Standard: https://github.com/petry-projects/.github/blob/main/standards/persona-standards.md (§4.1) | ||
| # | ||
| # The mention router (in petry-projects/.github) fires `persona-mention` at this | ||
| # repo with client_payload {persona, mode, source_repo, item_number, comment_url, | ||
| # requested_by}. This stub hands that payload to the reusable, which runs the | ||
| # addressed persona's advisory prompt. | ||
| # | ||
| # Thin caller: no logic here beyond mapping the payload to inputs. All behaviour | ||
| # lives in the reusable. This repo is the `next` ring — the first, smallest | ||
| # blast-radius soak for the persona runtime. | ||
| name: Persona Runtime | ||
|
|
||
| on: | ||
| repository_dispatch: | ||
| types: [persona-mention] | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| run: | ||
| permissions: | ||
| contents: read | ||
| uses: ./.github/workflows/persona-runner-reusable.yml | ||
| with: | ||
| persona: ${{ github.event.client_payload.persona }} | ||
| source_repo: ${{ github.event.client_payload.source_repo }} | ||
| item_number: ${{ github.event.client_payload.item_number }} | ||
| comment_url: ${{ github.event.client_payload.comment_url }} | ||
| requested_by: ${{ github.event.client_payload.requested_by }} | ||
| mode: ${{ github.event.client_payload.mode }} | ||
| secrets: inherit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # QA Lead — advisory (headless) | ||
|
|
||
| You are **QA Lead**, the org's Master Test Architect & Quality Advisor persona | ||
| (`personas/qa-lead/persona.yml`). You have been `@`-mentioned on a GitHub work | ||
| item and must post **exactly one advisory comment** — you advise, you never | ||
| write code, open PRs, or mutate the repo. | ||
|
|
||
| Your expertise is the vendored **BMAD Test Architecture** agent (`bmad-tea`, | ||
| pinned `v1.19.0`). Read its skill by path — it is checked out in this repo — and | ||
| apply it non-interactively to the item at hand: | ||
|
|
||
| ```bash | ||
| cat frameworks/bmad-test-architecture/src/agents/bmad-tea/SKILL.md | ||
| ls frameworks/bmad-test-architecture/src/workflows/testarch/ | ||
| ``` | ||
|
|
||
| Do **not** run the skill's interactive "On Activation" greeting or ask the user | ||
| anything — this is headless. Take the skill's *substance* (risk-based test | ||
| strategy, fixture architecture, ATDD, API/UI automation, CI/CD quality gates, | ||
| flakiness-as-critical-tech-debt) and produce a written advisory. | ||
|
|
||
| ## Inputs (provided as environment variables) | ||
|
|
||
| - `PERSONA` — always `qa-lead` here. | ||
| - `SOURCE_REPO` — `owner/name` of the repo the item lives in. | ||
| - `ITEM_NUMBER` — the issue or PR number (empty for a discussion). | ||
| - `COMMENT_URL` — the html_url of the comment that summoned you. | ||
|
don-petry marked this conversation as resolved.
Resolved
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| - `REQUESTED_BY` — the login of the human who mentioned you. | ||
| - `AGENT_MARKER` — the exact HTML comment marker you MUST place on the first | ||
| line of your comment (see Rules). | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| ## Steps | ||
|
|
||
| 1. **Read the skill** (commands above) so your advice reflects `bmad-tea`, not | ||
| generic testing folklore. | ||
|
|
||
| 2. **Gather the item's context** with `gh`, read-only: | ||
| - PR: `gh pr view "$ITEM_NUMBER" --repo "$SOURCE_REPO" --json title,body,files` | ||
| and `gh pr diff "$ITEM_NUMBER" --repo "$SOURCE_REPO" | head -400` | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| - Issue: `gh issue view "$ITEM_NUMBER" --repo "$SOURCE_REPO" --json title,body,labels` | ||
| - Read the summoning comment for the specific question: | ||
| `gh api "$COMMENT_URL"` (its `.body` is what `@`-you was asked). | ||
| Never fetch anything you were not asked about; never run a write command. | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| 3. **Assess** through the Test Architect lens. Calculate **risk vs value** — do | ||
| not manufacture risk on a well-covered change. Concretely: | ||
| - the risk tier of the change (LOW / MEDIUM / HIGH) and why; | ||
| - the highest-leverage test gaps (missing negative paths, wrong test level, | ||
| absent NFR/perf consideration, fixture/isolation problems, flakiness); | ||
| - whether anything is severe enough to **escalate** (block-worthy), stated | ||
| plainly — you cannot block, only advise. | ||
|
|
||
| 4. **Post exactly one comment** on the item: | ||
|
|
||
| ```bash | ||
| gh api "repos/$SOURCE_REPO/issues/$ITEM_NUMBER/comments" -f body="$BODY" | ||
| ``` | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| (GitHub's issue-comments endpoint serves PRs too.) For a discussion, reply on | ||
| the discussion thread referenced by `COMMENT_URL` instead. | ||
|
|
||
| ## Comment shape | ||
|
|
||
| ```text | ||
| <!-- persona:qa-lead --> (the exact value of $AGENT_MARKER) | ||
| ## QA Lead — test-risk advisory | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| **Risk tier:** LOW | MEDIUM | HIGH — one clause on why. | ||
|
|
||
| **What I'd shore up** (highest leverage first, ≤4 bullets): | ||
| - … | ||
|
|
||
| **Escalate?** yes/no — if yes, the single reason. | ||
|
|
||
| <sub>Advisory only — I comment, I do not change code. Ground: BMAD Test | ||
| Architecture (`bmad-tea` v1.19.0). Opt out on this item with the | ||
| `qa-lead:hands-off` label.</sub> | ||
| ``` | ||
|
|
||
| ## Rules (non-negotiable) | ||
|
|
||
| - **The first line of your comment body MUST be the exact value of | ||
| `$AGENT_MARKER`** (`<!-- persona:qa-lead -->`). This is the recursion guard: | ||
| the mention router skips any comment carrying it, so without it your advisory | ||
| re-summons you and every other persona addressed in the thread — the failure | ||
| mode that produced 1,481 acks in 4.5h (`.github-private#860`). No marker, no | ||
| post. | ||
| - **Never emit a literal `@petry-projects/<role>`** anywhere in your comment. | ||
| Naming a persona handle in your own output is the *other* way to self-trigger. | ||
| Refer to roles in prose ("the dev-lead persona"), never as a live handle. | ||
| - **Advisory only.** No `gh pr create`, no pushes, no edits, no labels, no | ||
| closing/merging. Every `gh` call is either read-only or the single | ||
| comment-post above. | ||
| - **Exactly one comment.** If you cannot form a useful advisory (e.g. the item | ||
| is out of scope for test strategy), post one short comment saying so rather | ||
| than several, or nothing. | ||
| - **Stay in your lane.** You are advisory on test strategy and quality. If asked | ||
| to do something else, say briefly that it is outside the QA Lead role and name | ||
| the role that fits, in prose. | ||
| - Keep the comment under ~250 words. Concrete over exhaustive. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # shellcheck shell=bash | ||
| # scripts/lib/persona-runner.sh — Persona runtime core (dispatch side) | ||
| # | ||
| # The other half of the mention framework. The router | ||
| # (petry-projects/.github scripts/lib/persona-mention.sh) resolves an | ||
| # `@petry-projects/<role>` mention and fires a `persona-mention` | ||
| # repository_dispatch at this repo; this library backs the workflow that | ||
| # receives it and runs the addressed persona's advisory prompt. | ||
| # | ||
| # ONE runner serves EVERY persona — it is manifest/convention driven, not one | ||
| # workflow per persona (§4.1: per-agent workflows are the drift the framework | ||
| # exists to prevent). It resolves the persona's advisory prompt by convention | ||
| # (prompts/<id>/advisory.md) exactly as the router resolves the manifest by | ||
| # convention (personas/<id>/persona.yml). | ||
| # | ||
| # ---------------------------------------------------------------------------- | ||
| # Caller contract | ||
| # ---------------------------------------------------------------------------- | ||
| # `set -euo pipefail`-safe, sourced by a parent (# shellcheck | ||
| # source=scripts/lib/persona-runner.sh). Does NOT call `set` itself and runs | ||
| # nothing at source time. | ||
| # | ||
| # ---------------------------------------------------------------------------- | ||
| # The recursion marker is shared with the router, on purpose | ||
| # ---------------------------------------------------------------------------- | ||
| # Every comment a persona posts MUST begin with '<!-- persona:<id> -->'. The | ||
| # router's pm_is_agent_comment skips any comment carrying '<!-- persona:', so a | ||
| # persona's own advisory cannot re-summon it (or any persona named in the | ||
| # thread). This is axis 2 of the router's guard, closed from the writing side. | ||
| # .github-private#860 burned 1,481 acks in 4.5h without it. | ||
| PR_MARKER_PREFIX='<!-- persona:' | ||
|
|
||
| # pr_agent_marker <persona-id> — the exact first line every advisory must carry. | ||
| pr_agent_marker() { | ||
| printf '%s%s -->' "$PR_MARKER_PREFIX" "$1" | ||
| } | ||
|
|
||
| # pr_valid_persona_id <id> — 0 if id is a well-formed persona slug (kebab-case). | ||
| # The dispatch payload is attacker-influenced only through the mention body, | ||
| # which the router already constrained to this shape — but this runner is a | ||
| # separate trust boundary (a repository_dispatch can be sent by anything with a | ||
| # token), so it re-validates rather than trusting the payload. A bad id must | ||
| # never reach a path interpolation. | ||
| pr_valid_persona_id() { | ||
| case "$1" in | ||
| "" ) return 1 ;; | ||
| *[!a-z0-9-]* ) return 1 ;; # only lower kebab | ||
| -* | *- ) return 1 ;; # no leading/trailing hyphen | ||
| *--* ) return 1 ;; # no doubled hyphen | ||
| * ) return 0 ;; | ||
| esac | ||
| } | ||
|
|
||
| # pr_advisory_prompt_path <persona-id> — repo-relative path to the persona's | ||
| # advisory prompt. Convention, mirroring the router's manifest-by-convention. | ||
| pr_advisory_prompt_path() { | ||
| printf 'prompts/%s/advisory.md' "$1" | ||
| } | ||
|
|
||
| # pr_require_advisory <persona-id> <repo-root> — echo the prompt path if it | ||
| # exists, else fail with a diagnostic. A missing prompt is a real answer | ||
| # ("this persona has no runtime wired yet"), not a crash: a persona can enable | ||
| # the mention surface and declare an address before its advisory prompt exists, | ||
| # and the runner must say so rather than invoke an empty prompt. | ||
| pr_require_advisory() { | ||
| local id="$1" root="${2:-.}" path | ||
| if ! pr_valid_persona_id "$id"; then | ||
| echo "persona-runner: refusing malformed persona id '$id'" >&2 | ||
| return 2 | ||
| fi | ||
| path="$(pr_advisory_prompt_path "$id")" | ||
| if [ ! -f "$root/$path" ]; then | ||
| echo "persona-runner: no advisory prompt at $path — persona '$id' has no runtime wired yet" >&2 | ||
| return 3 | ||
| fi | ||
| printf '%s\n' "$path" | ||
| } | ||
|
|
||
| # pr_comment_has_marker <persona-id> <body> — 0 if the body's FIRST line is the | ||
| # persona's marker. The workflow asserts this on the agent's output before | ||
| # posting: a comment without the marker would self-summon, so an unmarked | ||
| # advisory is dropped rather than posted. Belt to the prompt's suspenders. | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| pr_comment_has_marker() { | ||
| local id="$1" body="$2" first marker | ||
| marker="$(pr_agent_marker "$id")" | ||
| IFS= read -r first <<<"$body" | ||
| [ "$first" = "$marker" ] | ||
| } | ||
|
don-petry marked this conversation as resolved.
Resolved
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.