| <!-- SPDX-License-Identifier: Apache-2.0 |
| https://www.apache.org/licenses/LICENSE-2.0 --> |
| |
| <!-- START doctoc generated TOC please keep comment here to allow auto update --> |
| <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> |
| |
| - [`tools/change-request/`](#toolschange-request) |
| - [Prerequisites](#prerequisites) |
| - [What this is](#what-this-is) |
| - [Relationship to `contract:source-control`](#relationship-to-contractsource-control) |
| - [Today's adapters](#todays-adapters) |
| - [Interface](#interface) |
| - [`list_open(filter) to [proposal_summary]`](#list_openfilter-to-proposal_summary) |
| - [`get(id) to proposal`](#getid-to-proposal) |
| - [`get_discussion(id) to discussion`](#get_discussionid-to-discussion) |
| - [`post_review(id, verdict, body) to ok`](#post_reviewid-verdict-body-to-ok) |
| - [`land(id, strategy) to landed_ref`](#landid-strategy-to-landed_ref) |
| - [`reject(id, reason) to ok`](#rejectid-reason-to-ok) |
| - [`status(id) to {state, checks, mergeable}`](#statusid-to-state-checks-mergeable) |
| - [Generic lifecycle verbs](#generic-lifecycle-verbs) |
| - [Skills that consume this contract](#skills-that-consume-this-contract) |
| - [ASF defaults](#asf-defaults) |
| - [Configuration](#configuration) |
| |
| <!-- END doctoc generated TOC please keep comment here to allow auto update --> |
| |
| <!-- SPDX-License-Identifier: Apache-2.0 |
| https://www.apache.org/licenses/LICENSE-2.0 --> |
| |
| # `tools/change-request/` |
| |
| **Capability:** contract:change-request |
| |
| **Kind:** interface |
| |
| **Vendor:** agnostic |
| |
| ## Prerequisites |
| |
| - **Runtime:** None — this directory is a Markdown contract spec; no executable code ships here. It is read by the framework, not run. |
| - **CLIs / credentials / network:** Provided entirely by the resolved adapter — the sibling backend named by `change_request.backend` in `project.md`. The ASF default is GitHub (`tools/github/`); the SVN-first defaults are `tools/jira-patch/` and `tools/mail-patch/`, both of which land through the `contract:source-control` adapter (`tools/asf-svn/`). See each adapter for its concrete prerequisites. |
| |
| ## What this is |
| |
| The framework's **change-proposal** adapter contract: the seam that |
| lets a project drive PR-shaped review-and-merge work over a backend |
| that is *not* a GitHub pull request. Today every `pr-management-*` |
| skill is hardwired to `gh pr` — `gh pr list`, `gh pr view`, |
| `gh pr diff`, `gh pr review`, `gh pr merge` are woven directly into |
| the skill bodies. That is fine for the ASF's GitHub-hosted projects, |
| but it forecloses three large classes of adopter: |
| |
| - **JIRA-tracker projects.** `contract:tracker` abstracts the *issue* |
| side across GitHub and Atlassian, but Jira has no pull-request |
| model. A project whose issues live in JIRA and whose code lives in |
| SVN has patches attached to issues, not PRs — and no seam to plug |
| them into `pr-management-*`. |
| - **SVN-first ASF projects.** They lack both the PR object and the |
| hosted review surface. A committer applies a contributor's patch |
| with `svn patch` and commits it; the "review thread" is a JIRA |
| comment stream or a `dev@` mail thread. |
| - **`[PATCH]` mail-thread projects.** The oldest review mechanism at |
| the ASF (and in the wider open-source world) is a patch posted to |
| the developer list. It has a diff, a discussion, and a lifecycle, |
| but no forge behind it at all. |
| |
| This contract models **the unit that is forge-shaped today (a pull |
| request)** as a backend-neutral *change proposal*: a diff plus |
| metadata, a review discussion, and a lifecycle — decoupled from the |
| backend that stores them. It declares the verbs the generic skill |
| bodies call; the concrete `gh pr` invocation, the JIRA REST calls, |
| or the mbox parsing stay inside each adapter directory. |
| |
| The contract is read by the framework, not by humans during normal |
| operation. New adopters declare `change_request.backend: <adapter>` |
| in `projects/<project>/project.md`; the skills resolve that to a |
| sibling directory under `tools/` and call the verbs named here. |
| Adopters on GitHub inherit the `gh pr` behaviour verbatim. |
| |
| > **Naming.** [Issue #669](https://github.com/apache/magpie/issues/669) |
| > proposed this contract under the working name |
| > `contract:change-proposal`. It ships as `contract:change-request` |
| > to match the name introduced by |
| > [#678](https://github.com/apache/magpie/pull/678), which first |
| > registered the contract in the vendor-neutrality scorer. The two |
| > names denote the same seam; `change-request` is canonical. |
| |
| ## Relationship to `contract:source-control` |
| |
| Change-request is the **review + merge gate**; source-control is the |
| **branch / commit / diff / push** substrate underneath it. They are |
| distinct contracts because their backends differ: |
| |
| - On GitHub the merge gate *is* the forge — `gh pr merge` both lands |
| the diff and closes the proposal in one call, so `tools/github/` |
| implements both contracts. |
| - On an SVN-first project the merge gate and the commit substrate |
| are two different systems. `land` on the `jira-patch` and |
| `mail-patch` adapters **delegates the actual commit to |
| `contract:source-control`** (`tools/asf-svn/` → `svn patch` + |
| `svn commit`), then records the outcome back on the review surface |
| (a JIRA transition, a `dev@` reply). The change-request adapter |
| owns the *proposal lifecycle*; source-control owns the *bytes on |
| the trunk*. |
| |
| The `land` verb is the only place the two contracts touch. Every |
| other verb (`list_open`, `get`, `get_discussion`, `post_review`, |
| `reject`, `status`) is pure change-request. |
| |
| ## Today's adapters |
| |
| | Adapter | Directory | Vendor | Status | `land` path | |
| |---|---|---|---|---| |
| | GitHub pull request | [`tools/github/`](../github/) | GitHub | Shipping | `gh pr merge` (forge lands + closes) | |
| | JIRA patch | [`tools/jira-patch/`](../jira-patch/) | Atlassian | Shipping | delegates to `contract:source-control` (`svn patch` + `svn commit`) | |
| | `dev@` `[PATCH]` thread | [`tools/mail-patch/`](../mail-patch/) | email | Shipping | delegates to `contract:source-control` (`svn patch` + `svn commit`) | |
| | GitLab merge request | `tools/gitlab-mr/` *(planned)* | GitLab | Placeholder | `glab mr merge` | |
| | Gerrit change | `tools/gerrit/` *(planned)* | Gerrit | Placeholder | `git review` / submit | |
| | None | `tools/change-request-none/` *(planned)* | — | Placeholder | raises `NotApplicable`; the skill degrades to read-only | |
| |
| Three adapters ship today across three distinct backend vendors |
| (GitHub, Atlassian, email), so the contract is vendor-neutral by the |
| `MIN_VENDORS = 2` criterion the vendor-neutrality score applies. The |
| three placeholder rows are named — with a one-line `land` note each — |
| so an adopter who needs GitLab, Gerrit, or an explicit no-backend |
| declaration can author the adapter without re-deriving the contract. |
| |
| ## Interface |
| |
| Every change-request adapter exposes seven verbs. The names here are |
| the generic verbs the skills use; an adapter is free to name its |
| internal CLIs whatever fits its backend, as long as the skill-facing |
| surface uses these names. Output shapes are described in conceptual |
| terms — an adapter may return a language-native object as long as the |
| consuming skill can read the named fields. |
| |
| ### `list_open(filter) to [proposal_summary]` |
| |
| Enumerate the open change proposals matching a filter. |
| |
| - **When it fires.** Step 1 of `pr-management-triage` (and the |
| queue-fill step of every `pr-management-*` skill) — the sweep that |
| pulls the candidate set before classification. |
| - **Inputs.** `filter` — a dict of the queue narrowers the skills |
| support: `author`, `label`/`component`, `age`, `review-for-me`, |
| `stale`. The adapter translates these into its backend's query |
| language (a GitHub GraphQL search, a JQL `attachment is not EMPTY` |
| clause, a PonyMail `[PATCH]`-subject search). |
| - **Output.** An array of `proposal_summary` records, each at |
| minimum: `id`, `title`, `author`, `created`, `updated`, |
| `state` (a generic lifecycle verb, below), `labels[]`, and a |
| `permalink` the human can open. Ordering is newest-updated-first. |
| - **No-op case.** The `none` adapter returns `[]`; the skill reports |
| an empty queue and exits. An empty return and a no-access return |
| are indistinguishable by design — the skill surfaces the gap |
| without distinguishing the reason. |
| |
| ### `get(id) to proposal` |
| |
| Fetch one proposal's diff plus metadata. |
| |
| - **When it fires.** Whenever a skill pulls a single proposal out of |
| a group for individual handling, or resolves `pr:N` directly. |
| - **Inputs.** `id` — the adapter's opaque proposal identifier |
| (`123` for a GitHub PR number, `PROJECT-456` for a JIRA issue key |
| carrying a patch, an mbox `message_id` for a `[PATCH]` thread). |
| - **Output.** A `proposal` object extending `proposal_summary` with: |
| `diff` (unified-diff text, or a resolvable URL for very large |
| diffs), `base` (the target branch/trunk path), `commits[]` where |
| the backend has them (`[]` for a bare patch), `mergeable` (tri-state: |
| `clean` / `conflicting` / `unknown`), and `checks` (see `status`). |
| - **No-op case.** Returns `null` when the id is well-formed but the |
| proposal no longer exists (deleted PR, deleted JIRA attachment, |
| purged archive). Skills surface the gap; they do not retry. |
| |
| ### `get_discussion(id) to discussion` |
| |
| Read the review conversation attached to the proposal. |
| |
| - **When it fires.** Any step that needs the review history — |
| last-comment-by-viewer, unresolved-thread count, stale-reviewer |
| detection, mentor context. |
| - **Inputs.** `id`, as for `get`. |
| - **Output.** A `discussion` object: `comments[]` of |
| `{author, date, body, kind}` where `kind` is one of `comment`, |
| `review`, `approval`, `change-request`; plus `participants[]` and |
| `unresolved_count`. The adapter normalises its backend's native |
| shapes — GitHub review threads, JIRA comment stream, mail replies — |
| onto this one shape. |
| - **No-op case.** Returns an empty discussion (`comments: []`) when |
| the proposal has no review activity yet. |
| |
| ### `post_review(id, verdict, body) to ok` |
| |
| Post a review verdict + body onto the proposal. |
| |
| - **When it fires.** The confirmation step of `pr-management-triage` |
| (a *comment* disposition) and `pr-management-code-review` / |
| `pr-management-quick-merge` (an *approve* / *request-changes* |
| verdict). The skills always draft-then-confirm before this fires; |
| the adapter never gates — the skill does. |
| - **Inputs.** `id`; `verdict` — one of `comment`, `approve`, |
| `request-changes`; `body` — the review markdown. |
| - **Output.** `ok` sentinel (failure raises). On GitHub this is |
| `gh pr review`; on `jira-patch` it is a JIRA comment (with the |
| verdict encoded as a label transition); on `mail-patch` it is a |
| drafted reply through `contract:mail-create` (never auto-sent). |
| - **No-op case.** The `none` adapter raises `NotApplicable`; the |
| skill falls back to surfacing the drafted review to the maintainer |
| as copy-paste text. |
| |
| ### `land(id, strategy) to landed_ref` |
| |
| Merge / apply the proposal and close it in its accepted state. **This |
| is the only verb that touches `contract:source-control`.** |
| |
| - **When it fires.** The merge step — never from `pr-management-triage` |
| (triage never merges); from `pr-management-quick-merge` on explicit |
| per-PR confirmation, and from the maintainer's own merge command. |
| - **Inputs.** `id`; `strategy` — one of `merge`, `squash`, `rebase` |
| (backends that cannot honour the requested strategy fall back to |
| their only supported one and report which they used). |
| - **Output.** `landed_ref` — the resulting commit SHA / revision the |
| land produced, so the skill can cross-link it on the proposal and |
| on any tracking issue. |
| - **Backend behaviour.** |
| - `github`: `gh pr merge --<strategy>` — the forge both lands the |
| diff and closes the PR atomically. |
| - `jira-patch` / `mail-patch`: **delegates to |
| `contract:source-control`.** The adapter fetches the diff via |
| `get`, calls the source-control adapter's `apply` + `commit` |
| (`tools/asf-svn/` → `svn patch <file>` then `svn commit`), and on |
| success records the landed revision back onto the review surface |
| (a JIRA *Resolved/Fixed* transition, a `dev@` "applied in rNNNNN" |
| reply). Conflict handling and apply-idempotency live in the |
| source-control adapter, not here. |
| - **No-op case.** The `none` adapter raises `NotApplicable`; the |
| skill emits the paste-ready backend command for the maintainer to |
| run by hand (the framework's deliberately-deferred Agentic Autonomous posture — |
| no skill lands autonomously). |
| |
| ### `reject(id, reason) to ok` |
| |
| Decline the proposal without landing it. |
| |
| - **When it fires.** The *close* disposition of `pr-management-triage` |
| (stale / superseded / out-of-scope PRs), after draft-then-confirm. |
| - **Inputs.** `id`; `reason` — the closing rationale captured from |
| the triage discussion. |
| - **Output.** `ok` sentinel. On GitHub `gh pr close` with the reason |
| as a comment; on `jira-patch` a *Won't Fix* / *Rejected* transition |
| carrying the reason; on `mail-patch` a drafted reply with **no |
| commit** (the absence of a `land` is the rejection). |
| - **No-op case.** The `none` adapter raises `NotApplicable`. |
| |
| ### `status(id) to {state, checks, mergeable}` |
| |
| Read the CI / mergeable status of the proposal. |
| |
| - **When it fires.** Classification in `pr-management-triage` (the |
| CI-rerun / mark-ready gates) and the all-gates-green attestation in |
| `pr-management-quick-merge`. |
| - **Inputs.** `id`. |
| - **Output.** `state` (a generic lifecycle verb); `checks` — one of |
| `passing`, `failing`, `pending`, `none`; `mergeable` — `clean` / |
| `conflicting` / `unknown`. |
| - **Graceful degradation.** Backends without a CI/mergeable concept |
| (a bare `[PATCH]` mail thread; a JIRA patch with no attached |
| pipeline) return `checks: none`, `mergeable: unknown`. Skills treat |
| `none`/`unknown` as *"do not gate on backend status"* and fall back |
| to a human judgement prompt rather than blocking — this is the |
| answer to #669's *"graceful degradation for non-GitHub CI / |
| mergeable status"* open question. |
| |
| ## Generic lifecycle verbs |
| |
| The skills speak in generic verbs about a proposal's lifecycle. The |
| adapter maps its backend's native states onto these verbs. |
| |
| | Verb | Meaning | GitHub-native | JIRA-patch-native | mail-patch-native | |
| |---|---|---|---|---| |
| | `open` | Proposed, awaiting first review. | `OPEN`, no reviews | issue *Open* + patch attached | `[PATCH]` posted, no replies | |
| | `under-review` | Review in progress; comments present, no verdict. | `OPEN` + comments | issue *In Review* | thread has replies, no LGTM | |
| | `changes-requested` | A reviewer asked for changes. | `CHANGES_REQUESTED` | *Needs work* label | reply requesting changes | |
| | `approved` | Cleared to land, not yet landed. | `APPROVED` | *Reviewed* label | `LGTM` reply | |
| | `landed` | Applied to the target branch/trunk. Terminal, success. | `MERGED` | *Resolved/Fixed* | "applied in rNNNNN" reply | |
| | `rejected` | Declined without landing. Terminal, failure. | `CLOSED` unmerged | *Won't Fix* | reply, no commit | |
| | `superseded` | Replaced by a newer proposal. Terminal. | `CLOSED` + successor link | *Duplicate* | newer `[PATCH vN]` | |
| | `unknown` | Adapter cannot determine state. | n/a | n/a | n/a | |
| |
| The map is **adapter-internal**. Skills never write `MERGED` or |
| *Won't Fix* — they write `landed` and `rejected`, and the adapter |
| resolves the native transition. An adapter that needs a finer |
| internal model may add sub-states, as long as the contract-facing |
| verbs are what the skills see. |
| |
| ## Skills that consume this contract |
| |
| The change-request contract is the backend seam for the |
| `pr-management-*` family. Today the call sites are `gh pr`; this |
| contract is the interface those call sites resolve through. |
| |
| | Skill | Verbs used | |
| |---|---| |
| | [`pr-management-triage`](../../skills/pr-management-triage/SKILL.md) | `list_open`, `get`, `get_discussion`, `status`, `post_review` (comment), `reject` (close). **Never** `land` — triage does not merge. | |
| | [`pr-management-code-review`](../../skills/pr-management-code-review/SKILL.md) | `get`, `get_discussion`, `post_review` (approve / request-changes). | |
| | [`pr-management-quick-merge`](../../skills/pr-management-quick-merge/SKILL.md) | `list_open`, `get`, `status`, `post_review` (approve), `land` (on per-PR confirmation). | |
| | [`pr-management-mentor`](../../skills/pr-management-mentor/SKILL.md) | `get`, `get_discussion`, `post_review` (comment). | |
| | [`pr-management-stats`](../../skills/pr-management-stats/SKILL.md) | `list_open` (read-only queue metrics). | |
| |
| `pr-management-triage` is refactored in this PR to name the contract |
| verbs and document the GitHub binding as the *resolution* of those |
| verbs (see its **Change-request contract binding** section). The |
| remaining `pr-management-*` skills continue to call `gh pr` directly; |
| their call sites resolve to the same GitHub adapter, and a later PR |
| routes their prose through the verbs the way this one does for triage. |
| |
| ## ASF defaults |
| |
| | Project shape | `change_request.backend` | `land` resolution | |
| |---|---|---| |
| | GitHub-hosted (Airflow, most TLPs) | `github` | `gh pr merge` | |
| | JIRA + SVN | `jira-patch` | `svn patch` + `svn commit` via `tools/asf-svn/` | |
| | `dev@` `[PATCH]` workflow | `mail-patch` | `svn patch` + `svn commit` via `tools/asf-svn/` | |
| |
| GitHub is the default because it is what the shipping `pr-management-*` |
| skills are tested against. The two SVN-first adapters exist so that a |
| JIRA-tracker or patch-by-mail project can adopt the same skills; both |
| route their terminal `land` through the project's declared |
| `contract:source-control` adapter rather than owning the commit. |
| |
| ## Configuration |
| |
| Every adopter declares its change-request backend in |
| `projects/<project>/project.md` under the `change_request` block: |
| |
| ```yaml |
| # change_request — proposed-change review + merge gate |
| # ASF default: github (gh pr) |
| change_request: |
| backend: github # github | jira-patch | mail-patch | gitlab | gerrit | none |
| land_via: forge # forge (backend lands) | source-control (delegate to VCS adapter) |
| review_channel: forge # forge | jira-comment | mailing-list |
| default_strategy: squash # merge | squash | rebase — the strategy `land` requests |
| gates: # which status signals block a land; omitted signals are advisory |
| - checks |
| - mergeable |
| ``` |
| |
| Field-by-field: |
| |
| - **`backend`** — names the adapter directory the skills resolve to. |
| The ASF default is `github` (resolves to `tools/github/`). |
| SVN-first adopters pick `jira-patch` or `mail-patch`; both set |
| `land_via: source-control`. |
| - **`land_via`** — where the terminal `land` commits. `forge` (the |
| GitHub default) means the backend both lands and closes; |
| `source-control` means `land` delegates the commit to the |
| project's `contract:source-control` adapter (`tools/asf-svn/` for |
| ASF SVN) and only records the outcome on the review surface. |
| - **`review_channel`** — where review comments live: `forge` (PR |
| threads), `jira-comment`, or `mailing-list`. Drives which surface |
| `get_discussion` and `post_review` read/write. |
| - **`default_strategy`** — the merge strategy `land` requests. |
| Backends that cannot honour it fall back and report which strategy |
| they used. |
| - **`gates`** — the `status` signals that block a land. A backend |
| that returns `checks: none` / `mergeable: unknown` for a listed |
| gate degrades that gate to advisory (see `status` graceful |
| degradation). |
| |
| The contract does not constrain how an adapter implements any of |
| these — only that the settings are present and respected. Adapters |
| may add backend-specific sub-keys under `change_request.<backend>:` |
| (e.g. `change_request.jira_patch.attachment_field`, |
| `change_request.mail_patch.patch_subject_prefix`). |
| |
| Adapter selection is **purely declarative**. The skill bodies do not |
| branch on `backend` — they call the verbs, and dispatch into the |
| adapter happens at the contract boundary. Adding `gitlab` later is a |
| new `tools/gitlab-mr/` directory, not a change to the skills. |