Skip to content
Closed
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
5 changes: 4 additions & 1 deletion .agents/skills/work-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,10 @@ harnesses or rerun a real failure to obtain green. Keep drafts cheap and batch
pushes. A ready PR synchronize event runs fast feedback with a deliberately
nonpassing final gate. After the last push, convert to draft and mark ready to
request complete candidate confirmation; opening/reopening ready also requests it. After cheap gates independent categories run concurrently; per-PR
cancellation cannot cancel another PR. Measure queue/elapsed time and wasted
cancellation cannot cancel another PR. For a native stack, use
`bun scripts/request-candidate.ts <pr>` after the final push/preflight instead of
a bare ready transition. It freezes the contributing prefix into the request;
missing/stale native request evidence rejects confirmation. Measure queue/elapsed time and wasted
cancellations before adding shards. Follow docs/decisions/ci-utility.md.

Assess missing main commits before refreshing. Revalidate affected proof after
Expand Down
19 changes: 15 additions & 4 deletions .agents/skills/work-issue/references/ci-and-merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ fresh candidate; selected job checkout and aggregate checks remain required.
## Read the actual head

For stacked PRs, apply [Stacked PR evidence](#stacked-pr-evidence) below;
native requests use `bun scripts/request-candidate.ts <pr>` to freeze the
contributing prefix into the ready event. Bare ready transitions lack that
request evidence. Before native merge, repeat the read-only
`bun scripts/request-candidate.ts --verify <pr>` and retain its prefix identity
alongside actual selected-job receipts. A failed request does not mark ready.
the `main` commands in this reference describe an ordinary PR or bottom layer.

Record the PR head and query live main independently. Run the comparison
Expand Down Expand Up @@ -215,10 +220,16 @@ Load `gh-stack` and follow the
Run local preflight against the immediate parent ref for each upper layer.
For every PR in the intended merge prefix, record its head and declared base
branch, independently query that base's live Git ref, and inspect its own
selected jobs/checkouts. A two-parent synthetic checkout must have that recorded
parent tip and PR head as parents. Keep contract head-only receipts separate.
Do not substitute live `main` for an upper layer's base or claim its CI tested
the eventual integrated trunk tree.
selected jobs/checkouts. Ordinary dependency PRs require that recorded parent
tip and PR head as synthetic parents. Native requests instead require the
supported exact checkout shape: either ordered immutable synthetic first
parents through the frozen trunk with each second parent equal to its frozen
member head, or exact own-parent/candidate parents with independently verified
frozen-trunk/member ancestry and immutable member evidence. Every actual merge
tree must be independently reconstructed. Run the read-only prefix verification immediately
before native merge. Keep own-parent contract head-only receipts separate;
neither parent-relative preflight nor final-tree equality replaces native
prefix proof. Actual integrated-tree/main reconciliation remains required.

Read back every included PR's release milestone and all issues it advances,
along with current heads, bases, checks and review state. Confirm the resolved
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4.2"
- name: Install native prefix verification dependencies
if: github.event.pull_request.stack != null
run: bun install --frozen-lockfile
- name: Match the CONTRACT title and label to the contract doc
env:
BASE_REF: origin/${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
GH_TOKEN: ${{ github.token }}
run: bun scripts/check-contract-pr.ts "$BASE_REF" "$HEAD_SHA"
60 changes: 60 additions & 0 deletions docs/decisions/ci-utility.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,66 @@ Agents must not do so, and this policy does not change protection settings.
The read-only [Get a pull request API](https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request)
accepts the existing `contents: read` permission; no privileged token/event is added.

### Native contributing-prefix identity (#2141)

The selected native policy freezes the trunk through the candidate, excluding
strictly higher layers. After the final push and local preflight, run
`bun scripts/request-candidate.ts <pr>` to place a bounded prefix descriptor in
the PR body before requesting its ready event. It records a nonce, repository,
stack, ordered PR numbers, heads, declared parent refs and exact trunk. It is
request context, never authority by itself. Missing descriptors reject native
confirmation with the required action; later live membership is never silently
substituted. Ordinary PR requests keep their existing workflow.

Final candidate validation and the contract gate independently compare the
descriptor with read-only stack/PR APIs and live refs. Candidate proof also
binds the actual run SHA and immutable checkout's ordered parents and tree.
The supported prefix contains one to six open contributing members. The
2026-10-17 maintainer ruling extends support to two exact checkout shapes:

- A two-parent synthetic chain ending exactly at the frozen trunk. Each
synthetic commit's second parent is its ordered frozen member head, member
heads contain their predecessors, and every synthetic tree equals a locally
reconstructed `git merge-tree --write-tree` result.
- For an upper candidate, a two-parent merge whose parents are exactly
`[frozen own parent, frozen candidate head]`. Independently compare each
contributing member's immutable commit with local Git, prove ancestry from
the exact frozen trunk through every ordered member head, and reconstruct
the actual merge tree with `git merge-tree --write-tree`.

The second shape is observed in real upper fixture #2286; it does not encode
lower synthetic commits in its first-parent history. Both shapes require the
same frozen/live prefix authority and exact actual checkout identity. Conflicts,
unavailable inputs, unsupported shapes and contradictory evidence reject;
matching final trees alone never qualify a candidate. The six-member limit
preserves the existing bounded API lookup. Git's
[merge-tree documentation](https://git-scm.com/docs/git-merge-tree) defines the
independent merge operation; support is limited to these verified shapes.

The contract gate compares the frozen head with its verified own parent;
proof selection always compares the frozen trunk to the requested head, even
when the native event names its immediate own parent. The event/request/head
bindings must agree before planning; live authority is independently checked
at admission/final verification. An upper-only diff never narrows selected
runtime coverage for its contributing lower layers. Categories,
freshness and retention obligations are unchanged. A higher layer may change
only if fresh evidence proves this prefix and tested identity unchanged. A
contributing merge/rebase, head/ref/order change, replaced request or changed
trunk requires a new request even for equal content. Repeat the read-only
`bun scripts/request-candidate.ts --verify <pr>` immediately before native merge
alongside actual-job and merge requirements. This check is not a lock; a failed
request does not mark the candidate ready.

[GitHub's event metadata](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/optimizing-ci-for-stacked-pull-requests)
and [stack APIs](https://docs.github.com/en/pull-requests/reference/stacked-pull-requests-apis-and-webhooks)
supply request context and independent membership. They do not themselves
freeze every contributing identity. Real ordinary and native hosted receipts
are required before calling an implementation qualified; historical rejected
or cancelled native runs remain unqualified. No privileged event/token, generic
proof cache or native merge queue is introduced. #2111's selected reservation
still covers one exact candidate; this verification never extends it to another
candidate or waives per-layer proof.

## Historical measurement and upstream guidance

The following guidance records the paid-provider investigation which motivated
Expand Down
75 changes: 75 additions & 0 deletions docs/process/evidence/native-merge-chain-replay.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"source_issue": "https://github.com/schlessera/brain-kit/issues/2141",
"scope": "Immutable historical native metadata checkout and independent ORT tree reconstruction; not passing native candidate qualification.",
"git": "git version 2.51.0",
"commits": [
{
"parents": [
"3fc6fea5e22304df2810348074c32b1983fbea46",
"c40f533e5f2277b961acb1d48b7b3742dd7ac11c"
],
"sha": "ff8cecd806cf17987c1fea05ce084c9cf8b8fbef",
"tree": "125491a2dc2407dbc042bc36975b94ed1621467c",
"reconstructed_tree": "125491a2dc2407dbc042bc36975b94ed1621467c",
"reconstruction_exit": 0,
"run": 38902174935,
"attempt": 1,
"event": "pull_request",
"metadata_job": 116976830747,
"actual_checkout_from_job_log": "ff8cecd806cf17987c1fea05ce084c9cf8b8fbef",
"run_head": "c40f533e5f2277b961acb1d48b7b3742dd7ac11c",
"qualification": "Historical metadata/shape evidence only; cancelled upper runs are not passing native proof."
},
{
"parents": [
"ff8cecd806cf17987c1fea05ce084c9cf8b8fbef",
"b11c43ff08c9490f5acce54873682333b2a2601a"
],
"sha": "1262a109997fb6b7861f50e52b8da7c5940df0ed",
"tree": "25c7dc9c39199610343f59dfc52fd4f9c944d792",
"reconstructed_tree": "25c7dc9c39199610343f59dfc52fd4f9c944d792",
"reconstruction_exit": 0,
"run": 38902179343,
"attempt": 1,
"event": "pull_request",
"metadata_job": 116976845098,
"actual_checkout_from_job_log": "1262a109997fb6b7861f50e52b8da7c5940df0ed",
"run_head": "b11c43ff08c9490f5acce54873682333b2a2601a",
"qualification": "Historical metadata/shape evidence only; cancelled upper runs are not passing native proof."
},
{
"parents": [
"1262a109997fb6b7861f50e52b8da7c5940df0ed",
"395ff3a0f3981f2da9cebb17aa6a4dc887a7a94a"
],
"sha": "acbacf7c4d73dc48af92eae90387d19a7fbb9cc1",
"tree": "8bb77e92333ef76ca963938e24e2c0e779070b4c",
"reconstructed_tree": "8bb77e92333ef76ca963938e24e2c0e779070b4c",
"reconstruction_exit": 0,
"run": 38902182348,
"attempt": 1,
"event": "pull_request",
"metadata_job": 116976855103,
"actual_checkout_from_job_log": "acbacf7c4d73dc48af92eae90387d19a7fbb9cc1",
"run_head": "395ff3a0f3981f2da9cebb17aa6a4dc887a7a94a",
"qualification": "Historical metadata/shape evidence only; cancelled upper runs are not passing native proof."
},
{
"parents": [
"acbacf7c4d73dc48af92eae90387d19a7fbb9cc1",
"0d658e7ca5a1ef98860ad1b79653782931a1b63c"
],
"sha": "72caa79f3408a5a56107f0b95cf12e138f7a3a16",
"tree": "f3261f4bcfb381eb1bced8a039aae97b01bda102",
"reconstructed_tree": "f3261f4bcfb381eb1bced8a039aae97b01bda102",
"reconstruction_exit": 0,
"run": 38902186914,
"attempt": 1,
"event": "pull_request",
"metadata_job": 116976871841,
"actual_checkout_from_job_log": "72caa79f3408a5a56107f0b95cf12e138f7a3a16",
"run_head": "0d658e7ca5a1ef98860ad1b79653782931a1b63c",
"qualification": "Historical metadata/shape evidence only; cancelled upper runs are not passing native proof."
}
]
}
75 changes: 75 additions & 0 deletions docs/process/evidence/native-own-parent-replay.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"source_issue": "https://github.com/schlessera/brain-kit/issues/2141",
"scope": "Immutable replay of observed own-parent checkout; not new live-prefix or passing full candidate qualification. Original run was superseded and fixtures returned to draft.",
"git": "git version 2.51.0",
"run": 39019059474,
"metadata_job": 117357054877,
"actual_checkout_from_job_log": "7f92440e874e53eae0ef3d0ee0302e0229facf7b",
"prefix": {
"version": 1,
"request": "17670a921e36ca5b8e989596666e69ab",
"repository": "schlessera/brain-kit",
"pr": 2286,
"stack": 2287,
"position": 2,
"trunk": {
"ref": "test/2141-qualification-trunk-07f4f991",
"sha": "41a00e0ab38937d752d320e932ba240bf2a19770"
},
"members": [
{
"number": 2285,
"ref": "test/2141-native-lower-07f4f991",
"head": "1d2408a9cffd6db11920828dfc36c0666e88fb75",
"parent": "test/2141-qualification-trunk-07f4f991"
},
{
"number": 2286,
"ref": "test/2141-native-upper-07f4f991",
"head": "111d118add10b4c79c51663960f339b35e6fd155",
"parent": "test/2141-native-lower-07f4f991"
}
]
},
"commits": [
{
"sha": "7f92440e874e53eae0ef3d0ee0302e0229facf7b",
"tree": "4bcc9f6665ed1dec863d40ee3b7aba1a80ea2ea2",
"parents": [
"1d2408a9cffd6db11920828dfc36c0666e88fb75",
"111d118add10b4c79c51663960f339b35e6fd155"
],
"immutable_api_matches_local_git": true
},
{
"sha": "1d2408a9cffd6db11920828dfc36c0666e88fb75",
"tree": "a108352ee1643c4963d8187e80ff5247b02b01cd",
"parents": [
"e4997f38f4aa986ab5dc08d515b8faa017ff72ad"
],
"immutable_api_matches_local_git": true
},
{
"sha": "111d118add10b4c79c51663960f339b35e6fd155",
"tree": "4bcc9f6665ed1dec863d40ee3b7aba1a80ea2ea2",
"parents": [
"1d2408a9cffd6db11920828dfc36c0666e88fb75"
],
"immutable_api_matches_local_git": true
}
],
"ancestry": [
{
"ancestor": "41a00e0ab38937d752d320e932ba240bf2a19770",
"descendant": "1d2408a9cffd6db11920828dfc36c0666e88fb75",
"exit": 0
},
{
"ancestor": "1d2408a9cffd6db11920828dfc36c0666e88fb75",
"descendant": "111d118add10b4c79c51663960f339b35e6fd155",
"exit": 0
}
],
"reconstructed_tree": "4bcc9f6665ed1dec863d40ee3b7aba1a80ea2ea2",
"reconstruction_exit": 0
}
16 changes: 13 additions & 3 deletions docs/process/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,12 @@ non-interactive operation. These project rules apply alongside it:
- The bottom PR targets `main`; each upper PR targets its immediate parent.
Run `bun run check:pr --base origin/main` at the bottom and
`bun run check:pr --base <parent-ref>` above it, after fetching or verifying
that parent. CI and contract gates already use the PR's declared base.
that parent. Ordinary dependency PRs use their declared base. Native Actions
evaluate against the stack trunk: request confirmation with
`bun scripts/request-candidate.ts <pr>` after preflight, freezing the exact
contributing prefix before the ready event. The contract gate separately
verifies the layer's own parent. Missing/unsupported prefix evidence rejects
confirmation; see the [native identity policy](../decisions/ci-utility.md#native-contributing-prefix-identity-2141).
Recheck the diff and affected proof after rebasing or retargeting.
- Each PR uses the repository template and carries its own title, labels,
release milestone and proof. Replace generated titles/bodies after
Expand Down Expand Up @@ -359,11 +364,16 @@ non-interactive operation. These project rules apply alongside it:
before a PR number. Confirm the resolved membership and inspect all included
PRs' current heads, bases, reviews, milestones and selected checks immediately
before merging. The installed command has no `--match-head-commit` flag;
repeat `bun scripts/request-candidate.ts --verify <pr>` for the candidate's
frozen contributing prefix alongside the actual selected-job identity checks.
do not describe that readback as an atomic head lock or bypass branch rules.
If native stacks are unavailable, report the CLI error and use ordinary PRs
only after deliberately reconciling their ancestry and bases.
- Upper-layer CI proves a merge with its parent branch, not with live `main`.
Record that actual parent/head/checkout tree. After merging, verify every
- Ordinary upper-layer CI proves its parent/head merge; native CI verifies the
verified checkout: either the frozen synthetic chain from the exact trunk,
or exact own-parent merge with independent frozen-trunk/member ancestry,
immutable member evidence and reconstructed tree. Record
every contributing head and actual synthetic parent/checkout/tree. After merging, verify every
included PR's merge result on `main`, the integrated commits/trees, issue
states and milestones, and automatic main-push proof. A queued stack is not
yet merged. Use the stack-specific guidance in
Expand Down
8 changes: 7 additions & 1 deletion scripts/candidate-proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
import { planFromEnvironment, proofCadence, proofSelections } from "./ci-plan";
import { checkout, type Checkout } from "./proof-inputs";
import { GitHubProofAPI, type API } from "./proof-reuse";
import { validateNativeCandidate } from "./native-candidate";

export async function validateCandidate(root: string, event: any, env: NodeJS.ProcessEnv,
api?: API, actual: Checkout = checkout(root)): Promise<void> {
const cadence = proofCadence(env, event);
if (cadence === "feedback") throw new Error("Candidate confirmation required: convert this PR to draft, then mark ready for review after the final push.");
if (cadence !== "confirmation") return;
const pr = event.pull_request;
if (pr?.stack) {
if (!api) throw new Error("Native candidate confirmation requires live read-only GitHub identity verification");
await validateNativeCandidate(root, event, env, api, actual);
return;
}
if (!Number.isSafeInteger(pr?.number) || pr.number < 1 || !pr.base?.ref || !pr.head?.sha || !pr.base?.sha ||
actual.sha !== env.GITHUB_SHA || JSON.stringify(actual.parents) !== JSON.stringify([pr.base.sha, pr.head.sha])) {
throw new Error("Candidate checkout does not match the requested head/base merge identity");
Expand All @@ -17,7 +23,7 @@ export async function validateCandidate(root: string, event: any, env: NodeJS.Pr
const live = await api.json(`pulls/${pr.number}`);
const base = await api.json(`git/ref/heads/${encodeURIComponent(pr.base.ref)}`);
const commit = await api.json(`git/commits/${actual.sha}`);
if (live.state !== "open" || live.draft !== false || live.head?.sha !== pr.head.sha ||
if (live.stack || live.state !== "open" || live.draft !== false || live.head?.sha !== pr.head.sha ||
live.base?.ref !== pr.base.ref || live.base?.repo?.full_name !== env.GITHUB_REPOSITORY || base.object?.sha !== pr.base.sha) {
throw new Error("Candidate became stale: the live PR head, readiness or base changed; request confirmation again");
}
Expand Down
20 changes: 19 additions & 1 deletion scripts/check-contract-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function parseLabels(raw: string | undefined): string[] {
}

if (import.meta.main) {
const [base, head] = process.argv.slice(2);
let [base, head] = process.argv.slice(2);
const title = process.env.PR_TITLE;
if (!base || !head || title === undefined) {
console.error(
Expand All @@ -98,6 +98,24 @@ if (import.meta.main) {
process.exit(2);
}

if (process.env.GITHUB_EVENT_PATH) {
const event = await Bun.file(process.env.GITHUB_EVENT_PATH).json();
if (event.pull_request?.stack) {
try {
if (!process.env.GH_TOKEN || !process.env.GITHUB_REPOSITORY) throw new Error("Native contract comparison requires read-only GitHub identity verification");
const { GitHubProofAPI } = await import("./proof-reuse");
const { verifyNativePrefix, ownNativeParent } = await import("./native-candidate");
const { checkout } = await import("./proof-inputs");
if (head !== event.pull_request.head?.sha || checkout(ROOT).sha !== head) throw new Error("Native contract checkout differs from the frozen candidate head");
base = ownNativeParent(await verifyNativePrefix(event, process.env,
new GitHubProofAPI(process.env.GITHUB_REPOSITORY, process.env.GH_TOKEN), true));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(2);
}
}
}

const changed = changedFiles(ROOT, base, head);
// A gate that cannot see the diff must not report success.
if (changed === null) process.exit(2);
Expand Down
Loading
Loading