Repository navigation
[Phase 3] spam-pr-guard workflow in DRY_RUN, with the section 4 row and interaction contract #1204
Description
Story
As a dev-lead,
I want ship spam-pr-guard on the fleet's standard vendored-reusable + caller-stub + canary-rings shape — the reusable in petry-projects/.github/.github/workflows/spam-pr-guard-reusable.yml, the stub template in standards/workflows/spam-pr-guard.yml, the paired tests workflow in .github/workflows/spam-pr-guard-tests.yml, the registry entry in standards/canary-rings.json, and the deploy wiring in scripts/deploy-standard-workflows.sh + scripts/lib/ring-pins.sh — plus the section-4 row and interaction contract,
so that the scorer runs on real events safely (metadata-only, no PR-code execution), ships through the canary rings next → ring0 → ring1 → stable, and the role is declared and CI-verified before any label or close is enabled.
Model: pr-auto-review (host petry-projects/.github, reusable .github/workflows/pr-auto-review-reusable.yml, stub standards/workflows/pr-auto-review.yml, registry entry standards/canary-rings.json:949-1017). The dry_run enforcement switch is a stub-level with: input, defaulting ON — Phase 5 flips it per-ring, no code change required.
Acceptance Criteria
- Reusable:
.github/workflows/spam-pr-guard-reusable.ymlis aon: workflow_call:entry point with:inputs.dry_run(bool, defaulttrue) wired toenv: DRY_RUN: ${{ inputs.dry_run }}(pattern.github/workflows/apply-repo-settings-reusable.yml:25-29, 85, 99);inputs.agent_ref(string, defaultspam-pr-guard/v1-stable) withactions/checkoutofrepository: petry-projects/.github, ref: ${{ inputs.agent_ref }}so the scorer and lib run at the pinned channel (pattern.github/workflows/add-to-project-reusable.yml:62-99);permissions: {}at the workflow top level; job-level permissions only —pull-requests: writeandissues: writeare required to post theneeds-info/spamcomment/label even whiledry_run: true(writes are gated byDRY_RUNinside the staged action, not at the token level);- a single job that reacts to
inputs.event(the stub forwards${{ github.event_name }}) and runsscripts/spam-pr-score.shfrom the checked-outagent_ref.
- No PR-code execution. The reusable performs no checkout of the PR head (API/metadata only;
actions/checkoutv7 defaults block fork-PR checkout inpull_request_target). The security invariant is called out explicitly in the interaction contract. - Staged action mapping is idempotent. At most one marker-keyed comment per PR, keyed by
<!-- spam-pr-guard:v1 action=<class> -->; re-edits do not post duplicates. - Caller stub (template):
standards/workflows/spam-pr-guard.ymlis a thin stub with:on: pull_request_target: types: [opened, edited, reopened]only (noschedule);permissions: {}top-level; job-level permissions as above;uses: petry-projects/.github/.github/workflows/spam-pr-guard-reusable.yml@spam-pr-guard/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref(the# NOSONAR … first-party channel refmarker is MANDATORY —scripts/seed-repo-template.sh:60-67refuses to emit a stub without it);with: { dry_run: true, agent_ref: spam-pr-guard/v1-stable };- the standard SOURCE OF TRUTH / Standard / Reusable / AGENTS — READ BEFORE EDITING header block, identical in shape to
standards/workflows/pr-auto-review.yml:1-81.
- Paired tests workflow:
.github/workflows/spam-pr-guard-tests.ymlis path-filtered onpull_request:andpush: branches: [main]watching.github/workflows/spam-pr-guard*.yml,scripts/lib/safety-checks.sh,scripts/spam-pr-score.sh,standards/spam-pr-guard.json,tests/test_spam_pr_*.bats,tests/fixtures/spam-pr-guard/**, andstandards/workflows/spam-pr-guard.yml. Concurrencyspam-pr-guard-tests-${{ github.ref }},cancel-in-progress: true, pinnedactions/checkout@3d3c42e5…v7.0.1,persist-credentials: false,permissions: contents: readat the job level. Model:.github/workflows/pr-auto-review-tests.yml+ the.github/workflows/safety-checks-tests.ymlshipped by Phase 1. - Registry entry:
standards/canary-rings.json .agents["spam-pr-guard"]is added withhost: petry-projects/.github,reusable: .github/workflows/spam-pr-guard-reusable.yml,run_workflow: <caller-stub display name>,ringswithnext: $host-sibling(resolves to.github-privatevia the registry's$host/$org_infratokens — mirrorpr-auto-reviewlines 955-983),ring0: $host,ring1: TalkTerm + bmad-bgreat-suite,stable: *. Thegateblock carries the standard#548defaults (baseline_window_days,baseline_spike_cap_multiple,benign_failure_classes: [],control.allow_pre_existing: false, three transitions). The ring1→stable transition setsrequire_confirmation: true(see Phase 5). - Ring-pins wiring:
spam-pr-guardis inserted alphabetically intoRING_REUSABLES=(...)atscripts/lib/ring-pins.sh:32-50; the bats case assertingRING_REUSABLES == .agents | keyspasses without edit. - Deploy wiring:
spam-pr-guard.ymlis added toDEPLOYABLE_WORKFLOWS=(...)atscripts/deploy-standard-workflows.sh:116-129. If audit treats the stub as required for the fleet,SKIP_SELF_MANAGED[".github"]and[".github-private"]are extended to includespam-pr-guard.ymlso both org-infra repos reconcile under their self-managed path. - Interaction contract + section-4 row.
interaction-contracts/spam-pr-guard.ymlis added (schema_version,role,kind: runtime,workflows,interaction.{triggers.events, emits, idempotency_key, concurrency_lane, stop_markers, budget}).budget: noneis justified (deterministic, no agent churn).idempotency_key = pr_url + head_sha; per-PR concurrency lane. A Class 1 row (notimer_role) is added todocs/agentic-interaction-model.md § 4. - Validators pass.
validate-interaction-model,validate-interaction-contracts.py, andvalidate-workflow-schedulesall pass. The role does not self-trigger (pull_request_targetreacts to human/fork events, not its own output; §7 rule 1 satisfied with noself_trigger_guards). shellcheck, the paired tests workflow, and.github/workflows/ci.ymlall pass on this PR.
Tasks / Subtasks
-
.github/workflows/spam-pr-guard-reusable.ymlwithinputs.dry_run+inputs.agent_ref+ theagent_refcheckout + metadata-only scoring (AC: Addressing PR comments #1, Add multi-agent isolation strategy using git worktrees #2) -
standards/workflows/spam-pr-guard.ymlthin stub with the mandatory NOSONAR marker and the SOURCE OF TRUTH header (AC: Add workflow, environment, and orchestration guidance #4) -
.github/workflows/spam-pr-guard-tests.ymlpaired tests workflow with the path filter (AC: Add stacked PR strategy and Epic-level workflow guidance #5) -
standards/canary-rings.json .agents["spam-pr-guard"]registry entry with the standard Define the release soak standard (ring-promotion gate): time floor + traffic-scaled sample + health #548 gate andrequire_confirmation: trueonring1→stable(AC: feat: add Structured Logging and CQRS standards #6) -
scripts/lib/ring-pins.shRING_REUSABLESalphabetical insertion (AC: docs: document branch protection rules, rulesets, and auto-merge policy #7) -
scripts/deploy-standard-workflows.shDEPLOYABLE_WORKFLOWS+ any requiredSKIP_SELF_MANAGEDentries (AC: Evolve Agentic Development: Integrate Claude Agent Teams with BMAD Workflow #8) -
interaction-contracts/spam-pr-guard.yml+ Class 1 row indocs/agentic-interaction-model.md § 4(AC: docs: Dependabot security-only update standards #9) - Confirm interaction-model, contract, and schedule validators pass and there is no self-trigger (AC: docs: add GitHub repository settings standards #10)
- shellcheck + bats + paired tests workflow (AC: docs: add CI/CD standards and workflow patterns #11)
Dev Notes
pull_request_targetmetadata-only is the sanctioned pattern;actions/checkoutv7 blocks fork checkout by default — called out as a security invariant in the contract (enhancement risk Add workflow, environment, and orchestration guidance #4).- Class by the section-3 discriminator: E and not S ⇒ Class 1, no timer_role. A
pull_request_targetsingle-role stub is a legacy single-role carve-out and carries no job qualifier — exactly one section-4 row. Table is a fixed-column markdown table (Workflow (path) | Class | timer_role | Justification); justification cells must be pipe-free. - Interaction contract shape: copy
interaction-contracts/pr-review.yml/dev-lead.yml—schema_version,role,kind: runtime,workflows,interaction.{triggers.events, emits, idempotency_key, concurrency_lane, stop_markers, budget}.budget: noneis acceptable here (deterministic, no agent churn) — justify it; do NOT invent a new numeric cap. dry_runstaging is a per-ring, per-consumer input, not a repo var. The stub carrieswith: { dry_run: true }; Phase 5's go/no-go flips the stub default (via the nextscripts/cut-standards-release.sh cut) oncerequire_confirmationclears onring1→stable. Keep writes gated INSIDE the staged action by checkingenv.DRY_RUNrather than at the token level — a repo can still raise the stub input to override per-ring without rebuilding permissions.scripts/canary-rollout.sh autocutwill mintspam-pr-guard/vX.Y.Z+ movespam-pr-guard/v1-nexton first merge, providedCANARY_AUTO_CUTis on and the registry entry (AC feat: add Structured Logging and CQRS standards #6) is already present. Seeding (first-ever cut) is a one-time plumbing step after this PR merges, done viascripts/canary-rollout.sh(orscripts/cut-release.shif the agent prefers the one-shot form). It is a repo mutation, not a file change, and cannot land from a PR diff.- The reusable's own
permissions.contents: readis required at the job level because of theagent_refcheckout;pull-requests: writeandissues: writeare required to post comments/labels.DRY_RUN=trueshort-circuits the write calls — the token holds the scope but the code does not spend it. - If the Phase 2 burst signal needs a read beyond
pull-requests+issues(org-wide open-PR search), grant only the minimal additional read (or pass a declared PAT viasecrets.inherit) — do not broaden write permissions; keep the metadata-only, least-privilege posture.
Project Structure Notes
Four new files (reusable, stub, tests workflow, interaction contract) + three edits (standards/canary-rings.json, scripts/lib/ring-pins.sh, scripts/deploy-standard-workflows.sh) + one doc row (docs/agentic-interaction-model.md § 4). Consumes scripts/lib/safety-checks.sh (Phase 1) and scripts/spam-pr-score.sh + standards/spam-pr-guard.json (Phase 2).
References
.github/workflows/pr-auto-review-reusable.yml+standards/workflows/pr-auto-review.yml(host-in-.githubreusable+stub pattern; AC Addressing PR comments #1, Add workflow, environment, and orchestration guidance #4).github/workflows/add-to-project-reusable.yml:62-99(agent_refcheckout pattern; AC Addressing PR comments #1).github/workflows/apply-repo-settings-reusable.yml:25-29, 85, 99(dry_runwiring pattern; AC Addressing PR comments #1)standards/canary-rings.json:949-1017(pr-auto-reviewentry — minimal shape for AC feat: add Structured Logging and CQRS standards #6)scripts/lib/ring-pins.sh:32-50(AC docs: document branch protection rules, rulesets, and auto-merge policy #7)scripts/deploy-standard-workflows.sh:57-129(AC Evolve Agentic Development: Integrate Claude Agent Teams with BMAD Workflow #8)scripts/seed-repo-template.sh:60-87(AC Add workflow, environment, and orchestration guidance #4 — the NOSONAR + pin fail-closed guard)docs/agentic-interaction-model.mddocs/adding-an-agentic-role.mdinteraction-contracts/pr-review.yml,interaction-contracts/dev-lead.ymlscripts/validate-interaction-model.sh- Org standard for automatically handling spam / low-effort outside PRs #1198
Likely target surface
.github/workflows/spam-pr-guard-reusable.yml.github/workflows/spam-pr-guard-tests.ymlstandards/workflows/spam-pr-guard.ymlstandards/canary-rings.jsonscripts/lib/ring-pins.shscripts/deploy-standard-workflows.shinteraction-contracts/spam-pr-guard.ymldocs/agentic-interaction-model.md
Story prepared by the BMAD Scrum Master (Bob) for epic #1200. Realigned to the vendored-reusable + caller-stub + canary-rings shape (per discussion with the user, 2026-10-01). Status: ready-for-dev (blocked_by #1203).
Activity