loop-notify-pr Specification (Draft)¶
Platform specification for PR notifications after loop finalize on pull_request targets.
Status: implemented (P1) — loop-notify-pr, notify_context_json in .github/actions/; PR-head finalize migration to open_pr pending.
| Layer | Document |
|---|---|
| Invariants | Loop Engineering Design |
Targets / pr_exclude |
Multi-Branch Loops Design |
| CI sweeper dogfood | CI Sweeper Workflow Design |
Problem¶
When CI fails on a human open PR, the loop opens a separate bot fix PR targeting the PR head branch (open_pr). The human PR author has no in-thread signal unless the platform posts on the human PR.
Goals¶
- Post or update a single marker comment on the human PR (
target_json.to.pr_number) when finalize runs for apull_requesttarget. - Include fix summary and link to the bot fix PR when finalize creates one (
open_pr). - Prompt the author to merge or close the bot fix PR (L2) or note that it was auto-merged (L3).
- Keep notification content platform-owned (Layers 1–2). Skill output is optional appendix only.
- Implement via shared
loop-notify-praction as a sibling step afterloop-finalizeinci-loop-agent(not per-caller shell logic).
loop-notify-pr uses notify_context_json for human PR comments only. PR description composition is owned by loop-finalize (render_pr_body.sh).
Non-Goals (v1)¶
- Notifications for
integration+open_pr(fix PR body is sufficient; no human PR in scope). - Per-PR opt-in labels (
pr_require/ci-sweeper-ok) — removed; usepr_excludeonly. - Auto-merge of the human PR (only the bot fix PR is auto-merged at L3).
- Inline review comments per changed line (reviewdog-style).
- External channels (Slack, email, PagerDuty).
Repository Prerequisites¶
| Prerequisite | Owner | Failure mode |
|---|---|---|
pr_enabled: true on ci-sweeper caller |
Loop maintainer | PR-head targets not enumerated |
pull-requests: write on finalize job |
Caller workflow | loop-notify-pr cannot post comment |
PR watch filters (pr_exclude)¶
Comma-separated deny tokens processed by loop-detect:
| Token | Behavior |
|---|---|
fork |
Exclude fork PRs |
draft |
Exclude draft PRs |
label:<name> |
Exclude PRs with label |
wip_title |
Exclude WIP title patterns |
Dogfood default (ci-sweeper):
pr_exclude: fork,draft,label:no-loop
pr_enabled: true
Bots excluded unless pr_include_bots lists them. No pr_require gate.
When loop-notify-pr Runs¶
| Condition | Notify |
|---|---|
target_json.mode == pull_request |
Yes |
target_json.to.pr_number present (human PR) |
Yes |
target_json.mode == integration |
No |
Finalize attempted (success, rejected, watch, error with PR context) |
Yes — all outcomes |
Detect should_run == false |
No |
| Execute skipped (budget, etc.) | No |
Invocation: sibling step in ci-loop-agent.yaml immediately after loop-finalize. Run with if: always() when pr_number is set and the finalize job executed.
Notification Content Layers¶
Layer 1 — Platform (required, machine-sourced)¶
| Field | Source |
|---|---|
| Loop name | loop_name input |
| Actor | GitHub App / bot login from token (GET /user); fallback github-actions on failure |
| Outcome | finalize outcome enum |
| Verdict | APPROVE / REJECT / empty |
| Reason | execute reason (checker reject reason or no-changes reason); — when empty |
| Bot fix PR | URL/number from finalize when open_pr succeeded |
| Branch | target_json.to.branch (PR head) |
| Human PR number | target_json.to.pr_number |
| Failed workflow | target_json.workflow_run_id + URL from detect result or event env |
| Failing job | detect failures[].job_name when present |
| Loop run URL | ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| Attempt | attempts / agent_loop_max_attempts |
| Level | L2 vs L3 (merge guidance vs auto-merge note) |
Layer 2 — Mechanical fix context (required when has_changes == true)¶
| Field | Source |
|---|---|
changed_files |
git diff --name-only vs checker baseline ref |
diff_stat |
git diff --stat (truncated) |
fix_summary |
Template: "Address CI failure in <job_name> (<workflow_name>)" from detect facts |
Reject / no-changes explanations belong in the Layer 1 Reason row, not under Fix context.
Omit ### Fix context when there is no mechanical diff and outcome is not watch.
L2 success messaging must include merge or close the bot fix PR guidance.
Appendix — Skill (optional)¶
When agent_report_overview / agent_report_summary are present in notify_context_json (from agent ## Overview / ## Summary), render them after the platform table — same narrative density as the bot fix PR body.
Legacy <!-- loop-agent-summary:v1 --> block via notify_context_json.agent_summary is used only when agent_report_summary is empty.
Comment Format¶
Marker: <!-- loop-notify-pr:v1:{loop_name} -->
Template (normative structure)¶
<!-- loop-notify-pr:v1:{loop_name} -->
## Loop notification: {loop_name}
| Field | Value |
| ---------- | -------------------------------------- |
| Outcome | `{outcome}` |
| Bot fix PR | [#{fix_pr}]({fix_pr_url}) or — |
| Verdict | `{verdict}` or — |
| Reason | `{reason}` or — |
| Actor | `{actor_login}` |
| Branch | `{to.branch}` |
| Failed run | [{workflow_name} #{run_id}]({run_url}) |
| Loop run | [actions run]({loop_run_url}) |
### Overview
{agent_report_overview when present}
### Summary
{agent_report_summary when present}
### Changes
{changed_files + diff_stat when agent narrative present}
### Fix context
{Layer 2 bullets when agent narrative absent}
**Next step (L2):** Merge or close the bot fix PR above, then re-run CI on this PR.
**Next step (L3):** Bot fix PR auto-merge enabled when checks pass.
Execute Output Extension¶
| Output | Type | Required | Description |
|---|---|---|---|
notify_context_json |
string (JSON) | always | Machine context for loop-notify-pr |
Schema unchanged — see prior revision for field list. Add fix_pr_url / fix_pr_number from finalize outputs when wired.
loop-notify-pr Action Contract (Draft)¶
Inputs/outputs unchanged from P1 except:
commit_sha— set when finalize pushed or opened PR from agent branch- Comment body includes bot fix PR link for
open_prfinalize
Failure policy: continue-on-error: true on notify step.
Implementation Phases¶
| Phase | Deliverable |
|---|---|
| P0 | Spec + design docs (this document) |
| P1 | loop-notify-pr, notify_context_json, pr_exclude filters |
| P2 | Dogfood ci-sweeper: open_pr to PR head + notify with fix PR link; remove pr_require |
| P3 | Optional agent_summary appendix in ci-sweeper skill reference |
Resolved decisions¶
| Topic | Decision |
|---|---|
| Opt-in | pr_exclude deny list only; no label opt-in |
| Human PR automerge | Never — L3 auto-merge applies to bot fix PR only |
| Delivery | open_pr to PR head branch; not direct push_head |
| Marker scope | <!-- loop-notify-pr:v1:{loop_name} --> |
Trigger thread UX (pr-revise)¶
When the workflow event carries github.event.comment.id (comment webhooks):
- Start ACK —
ci-loop-calleradds aneyesreaction on the triggering comment after detectshould_run=true(issue_comment and pull_request_review_comment only). - Done reply —
loop-notify-prposts a short result reply after the marker upsert:pull_request_review_comment→ REST thread reply (.../comments/{id}/replies)issue_comment→ follow-up PR conversation comment (REST has no issue-comment thread)
- Marker comment remains the run-level summary (
<!-- loop-notify-pr:v1:{loop_name} -->). - Auto-resolve of review threads stays out of scope.
Shared helpers: .github/actions/lib/loop/trigger_thread.sh.