Skip to content
Merged
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
158 changes: 147 additions & 11 deletions .github/workflows/branch-claim-check-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@
# half so arming it later is a one-word change in the caller, not a rewrite --
# but nothing should pass `refuse` until that measurement exists.
#
# ONE RESOLUTION, NOT TWO. Everything this job decides is decided by
# `scripts/branch_claim.py` in this repository, which is the same module the
# pre-push hook runs. A second implementation of the comparison is a second
# place for it to drift, and a local refusal that disagrees with a remote check
# is invisible until they disagree about a real push.
# TWO SOURCES. `work-ledger` reads the canonical lab node's GitHub check for the
# PR head. The database work ledger is read by the lab node; this job opens no
# database connection and only interprets the node's reported outcome.
# `file` runs `scripts/branch_claim.py`, the same module the pre-push hook runs,
# against the private ledger and its rolls. That path is retained only until
# the follow-up removal. This job implements no claim comparison of its own on
# either path. The lab node's resolution is a port of `scripts/claim_index.py`
# and `scripts/branch_claim.py` over the database rows, so a change to the
# resolution here has to land in the node as well until the pre-push hook asks
# the same node.
#
# `secrets: inherit` IS REQUIRED. The claim store lives in a PRIVATE workspace
# repository, and this job mints an installation token to read it. The claim
# `secrets: inherit` IS REQUIRED FOR `file`. The claim store lives in a PRIVATE
# workspace repository, and this job mints an installation token to read it. The claim
# index module that resolves it does NOT: it is vendored in this repository as
# `scripts/claim_index.py` beside the resolver (OMN-19722), and is fetched at the
# same pin. Fetching it unpinned from the store's default branch let a wording
Expand All @@ -47,7 +52,7 @@
# uses: OmniNode-ai/omniclaude/.github/workflows/branch-claim-check-reusable.yml@<immutable-sha>
# secrets: inherit
#
# WHAT THIS CHECK CANNOT SEE, measured on its own first run. The store is read
# WHAT `file` CANNOT SEE, measured on its own first run. The store is read
# from the private repository's DEFAULT BRANCH, so a claim row that exists only in
# a lane's working copy is invisible here until it is committed and merged. The
# first run of this workflow resolved its own branch as `unclaimed` for exactly
Expand All @@ -64,6 +69,26 @@ name: Branch Claim Check (reusable)
on:
workflow_call:
inputs:
claim_source:
description: "'file' reads the private ledger; 'work-ledger' reads the lab node's check."
type: string
required: false
default: file
lab_check_name:
description: Name of the canonical lab branch-claim check on the PR head.
type: string
required: false
default: branch-claim / work-ledger
lab_check_app_slug:
description: GitHub App slug trusted to publish the lab check.
type: string
required: false
default: onexbot-occ-writer
lab_check_wait_seconds:
description: Maximum seconds to wait for a completed lab check.
type: number
required: false
default: 900
mode:
description: >-
'record' (default) reports findings and exits 0. 'refuse' fails the
Expand Down Expand Up @@ -116,6 +141,7 @@ on:

permissions:
contents: read
checks: read

jobs:
branch-claim-check:
Expand All @@ -140,13 +166,117 @@ jobs:
&& fromJSON(vars.OMNI_PUBLIC_PR_RUNS_ON_JSON || '["ubuntu-latest"]')
|| fromJSON(vars.OMNI_TRUSTED_CI_RUNS_ON_JSON || '["self-hosted","omnibase-ci"]')
}}
timeout-minutes: 15
# Leave room for the default 900-second lab wait and its final diagnostic.
timeout-minutes: 20
steps:
- name: Validate the claim source
env:
CLAIM_SOURCE: ${{ inputs.claim_source }}
run: |
set -euo pipefail
case "$CLAIM_SOURCE" in
file|work-ledger) ;;
*)
echo "::error::Unknown claim_source '$CLAIM_SOURCE'. THE CHECK DID NOT RUN."
exit 1
;;
esac

# TRUST BOUNDARY. A check run's `app` is set by GitHub from the token that
# created it, not from the request body: only an installation token of the
# named App can publish a check run attributed to it. A workflow's
# GITHUB_TOKEN publishes as `github-actions`, and a person with write
# access publishes as whatever App their token belongs to, so neither can
# post a run this step accepts. Runs from any other App are counted,
# reported and ignored. The check name is percent-encoded with jq's
# `@uri`, which escapes `&`, `=`, `#` and every other reserved character,
# so it cannot add query parameters.
- name: Read the lab branch-claim check for this head
if: inputs.claim_source == 'work-ledger'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
LAB_CHECK_NAME: ${{ inputs.lab_check_name }}
LAB_CHECK_APP_SLUG: ${{ inputs.lab_check_app_slug }}
LAB_CHECK_WAIT_SECONDS: ${{ inputs.lab_check_wait_seconds }}
MODE: ${{ inputs.mode }}
run: |
set -euo pipefail
encoded_name="$(jq -rn --arg name "$LAB_CHECK_NAME" '$name | @uri')"
endpoint="repos/$REPO/commits/$HEAD_SHA/check-runs?check_name=$encoded_name&filter=all&per_page=100"
wait_seconds="$(jq -en --argjson wait "$LAB_CHECK_WAIT_SECONDS" '$wait | select(. >= 0) | ceil')"
deadline=$((SECONDS + wait_seconds))
selected=""
# Always attempt once, including when the configured wait is zero.
while :; do
if response="$(gh api --paginate "$endpoint")"; then
ignored="$(jq -s --arg app "$LAB_CHECK_APP_SLUG" \
'[.[].check_runs[] | select(.app.slug != $app)] | length' <<< "$response")"
echo "Ignored $ignored check run(s) from other apps; trusted app: $LAB_CHECK_APP_SLUG."
selected="$(jq -sc --arg app "$LAB_CHECK_APP_SLUG" \
'[.[].check_runs[] | select(.app.slug == $app and .status == "completed")] | sort_by(.completed_at) | last // empty' \
<<< "$response")"
if [[ -n "$selected" ]]; then
break
fi
else
echo "Lab check API request failed; retrying within the $LAB_CHECK_WAIT_SECONDS second deadline."
fi
remaining=$((deadline - SECONDS))
if ((remaining <= 0)); then
break
fi
sleep_for=20
if ((remaining < sleep_for)); then
sleep_for=$remaining
fi
sleep "$sleep_for"
if ((SECONDS >= deadline)); then
break
fi
done
if [[ -z "$selected" ]]; then
echo "Lab check source: $LAB_CHECK_NAME; app=$LAB_CHECK_APP_SLUG; head=$HEAD_SHA; no completed outcome line or html_url available."
echo "::error::Check '$LAB_CHECK_NAME' from app '$LAB_CHECK_APP_SLUG' did not complete for head $HEAD_SHA within $LAB_CHECK_WAIT_SECONDS seconds. The lab node did not answer for this head. THE CHECK DID NOT RUN."
exit 1
fi

summary="$(jq -r '.output.summary // ""' <<< "$selected")"
html_url="$(jq -r '.html_url // ""' <<< "$selected")"
first_line="${summary%%$'\n'*}"
printf '%s\n%s\n' "$html_url" "$first_line"
if [[ ! "$first_line" =~ ^branch-claim-outcome:\ (held-elsewhere|fence-behind|unidentified|held-by-pusher|unclaimed|no-ticket|did-not-run)\ ]]; then
echo "::error::Check '$LAB_CHECK_NAME' from app '$LAB_CHECK_APP_SLUG' has an invalid outcome line for head $HEAD_SHA. THE CHECK DID NOT RUN."
exit 1
fi
outcome="${BASH_REMATCH[1]}"
case "$outcome" in
did-not-run)
printf '%s\n' "$summary"
cause="${summary#*$'\n'}"
cause="${cause%%$'\n'*}"
echo "::error::$cause THE CHECK DID NOT RUN."
exit 1
;;
held-elsewhere|fence-behind|unidentified)
while IFS= read -r line; do
printf '::warning::%s\n' "$line"
done <<< "$summary"
if [[ "$outcome" != "unidentified" && "$MODE" == "refuse" ]]; then
exit 1
fi
;;
held-by-pusher|unclaimed|no-ticket) ;;
esac

# The HEAD commits, not the merge commit. The resolution reads the lane
# trailers of the commits the branch actually carries; a merge commit git
# synthesised carries none, and reading it would report every pull request
# as unidentified.
- name: Checkout the pull request head
if: inputs.claim_source == 'file'
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
Expand All @@ -158,6 +288,7 @@ jobs:
persist-credentials: false

- name: Set up Python 3.13
if: inputs.claim_source == 'file'
uses: actions/setup-python@v7
with:
python-version: '3.13'
Expand All @@ -167,6 +298,7 @@ jobs:
# side path -- mirrors public-repo-hygiene-reusable.yml and
# kb-doc-gate-reusable.yml.
- name: Resolve the gate pin
if: inputs.claim_source == 'file'
id: pin
env:
WORKFLOW_SHA: ${{ job.workflow_sha }}
Expand All @@ -187,7 +319,7 @@ jobs:
echo "branch-claim pin: $resolved_pin"

- name: Fetch the resolution (omniclaude canonical source)
if: github.repository != 'OmniNode-ai/omniclaude'
if: inputs.claim_source == 'file' && github.repository != 'OmniNode-ai/omniclaude'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: OmniNode-ai/omniclaude
Expand All @@ -201,7 +333,7 @@ jobs:
sparse-checkout-cone-mode: false

- name: Assert the branch-claim checkout is at the pin
if: github.repository != 'OmniNode-ai/omniclaude'
if: inputs.claim_source == 'file' && github.repository != 'OmniNode-ai/omniclaude'
env:
GATE_PIN: ${{ steps.pin.outputs.sha }}
run: |
Expand All @@ -221,6 +353,7 @@ jobs:
# request `owner/`, which fails in a way that reads like a transient
# network problem rather than a missing configuration. Name the cause.
- name: Resolve the private claim store repo
if: inputs.claim_source == 'file'
id: store-repo
env:
EXPLICIT: ${{ inputs.store_repo_name }}
Expand All @@ -240,6 +373,7 @@ jobs:
echo "name=${resolved}" >> "$GITHUB_OUTPUT"

- name: Mint token for the private claim store repo
if: inputs.claim_source == 'file'
id: store-token
uses: actions/create-github-app-token@v3
with:
Expand All @@ -259,6 +393,7 @@ jobs:
# no code is fetched from it (the index module is vendored here, at the
# pin, since OMN-19722). An unreadable store is a refusal.
- name: Fetch the claim store and its rolls
if: inputs.claim_source == 'file'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository_owner }}/${{ steps.store-repo.outputs.name }}
Expand All @@ -271,6 +406,7 @@ jobs:
sparse-checkout-cone-mode: false

- name: Resolve this branch against the claim store
if: inputs.claim_source == 'file'
env:
MODE: ${{ inputs.mode }}
BRANCH: ${{ github.head_ref }}
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/branch-claim-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
# local path rather than a pinned cross-repository SHA. Caller repositories pin
# an immutable omniclaude SHA instead; see the reusable workflow's header.
#
# OMN-20708: the reusable workflow can read the lab node's branch-claim check
# (`claim_source: work-ledger`, which opens no ledger) instead of the private
# markdown ledger. This caller stays on `file` until a completed
# `branch-claim / work-ledger` check from the writer App is visible on an
# omniclaude pull request head: a caller flipped before then waits out the whole
# window on every pull request and fails with THE CHECK DID NOT RUN. Observed
# 2026-10-13: none of omniclaude#2708, #2710, #2711 or #2672 carried that check.
# The flip is the one `claim_source` line below.
#
# It RECORDS. The operator ruling of 2026-09-13T09:11:33Z put the refusing half
# behind a measurement of the release path, so `mode` stays at its default here.

Expand All @@ -24,8 +33,11 @@ concurrency:

permissions:
contents: read
checks: read

jobs:
branch-claim-check:
uses: ./.github/workflows/branch-claim-check-reusable.yml
with:
claim_source: file
secrets: inherit
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4578,10 +4578,13 @@ jobs:
- 'scripts/hooks/pre-push-branch-claim'
- 'tests/scripts/test_branch_claim.py'
- 'tests/scripts/test_branch_claim_hook.py'
- 'tests/scripts/test_branch_claim_check_workflow.py'
- 'tests/scripts/test_lane_identity_canary.py'
- 'scripts/hooks/prepare-commit-msg-lane'
- '.github/workflows/ci.yml'
- '.github/workflows/branch-claim-check.yml'
- '.github/workflows/branch-claim-check-reusable.yml'
- '.pre-commit-config.yaml'
push: ['main', 'dev', 'hotfix/**']
check-deterministic-skills:
paths:
Expand Down Expand Up @@ -4885,6 +4888,13 @@ jobs:
tests/scripts/test_branch_claim_hook.py \
tests/scripts/test_lane_identity_canary.py -q

# OMN-20708: the reader of the lab node's branch-claim check (`claim_source:
# work-ledger`) runs its own Bash against a fake gh and real jq. Paired with
# the branch-claim-check-workflow pre-commit hook. This job's result is part
# of the required CI Summary context.
- name: Branch-claim check workflow reader (OMN-20708)
run: uv run pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -p no:randomly

# Folded from .github/workflows/check-deterministic-skills.yml (OMN-20074); runs when its original `paths:` change.
# Deterministic-Skill Routing Enforcement Gate (OMN-8749, wired under OMN-14808)
#
Expand Down
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,16 @@ repos:
always_run: true
stages: [pre-commit]

# OMN-20708: the branch-claim check's lab-node reader. SYNC with the ci.yml
# Branch Claim Gate step "Branch-claim check workflow reader (OMN-20708)".
- id: branch-claim-check-workflow
name: Branch-claim check reads the lab node's check (OMN-20708)
entry: bash scripts/uv-run-worktree-safe.sh pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -p no:randomly
language: system
pass_filenames: false
files: '^(\.github/workflows/branch-claim-check(-reusable)?\.yml|tests/scripts/test_branch_claim_check_workflow\.py|\.pre-commit-config\.yaml|\.github/workflows/ci\.yml)$'
stages: [pre-commit]

# OMN-20910: a review voter's model is declared only in the contract
# overlay. SYNC with the ci.yml refusal-surface step "Review voter model
# comes from the overlay (OMN-20910)".
Expand Down
57 changes: 57 additions & 0 deletions contracts/OMN-20708.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
schema_version: "1.0.0"
ticket_id: "OMN-20708"
title: "The branch claim check can answer from the database work ledger, with the markdown ledger absent"
summary: "omniclaude's reusable branch-claim workflow gains a claim_source input. With work-ledger it reads the check the lab node posts on the pull request head (branch-claim / work-ledger, from the writer App), opens no database connection and checks out no ledger; with file (the default, and what this repository's own caller still passes) it runs the existing markdown-ledger resolution. The caller flips to work-ledger in a later change, once a completed lab check is visible on an omniclaude head. This slice adds the test that runs the reader's own Bash, a named step for it in the Branch Claim Gate job (a member of the required CI Summary umbrella) and a check-only pre-commit hook."
is_seam_ticket: false
interface_change: false
interfaces_touched: []
requirements:
- id: "req-reader-needs-no-markdown-ledger"
statement: "With claim_source work-ledger the job runs no step that fetches or reads the markdown ledger, and an unheld branch passes."
acceptance:
- id: "AC1"
statement: "AC1: with work-ledger the only steps that run are the source validator and the lab-check reader, and the reader passes an unheld branch with no ledger present -- falsifier: uv run pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -k 'work_ledger_source_runs_no_ledger_step or ledger_absent_passes'"
- id: "req-reader-refuses-held-elsewhere"
statement: "A branch another lane holds is refused in refuse mode, and a missing, garbled or did-not-run lab answer never passes in either mode."
acceptance:
- id: "AC2"
statement: "AC2: held-elsewhere and fence-behind fail in refuse mode and only warn in record mode, and an absent, pending, foreign-app, garbled or did-not-run lab check fails in both modes -- falsifier: uv run pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -k 'claimed_elsewhere_refused or no_completed_matching_run_fails or garbled_first_line_fails or outcomes_follow_mode'"
- id: "req-reader-test-is-wired"
statement: "The reader's test file is a named step of a job the required CI Summary context covers and a check-only pre-commit hook."
acceptance:
- id: "AC3"
statement: "AC3: the test file is the step Branch-claim check workflow reader (OMN-20708) of the Branch Claim Gate job, its path filter names the caller, the reusable workflow and the test, and the branch-claim-check-workflow pre-commit hook runs it -- falsifier: uv run pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -k branch_claim_check_workflow_is_wired"
evidence_requirements:
- kind: "tests"
description: "The lab-check reader's Bash, run with a fake gh and real jq."
command: "uv run pytest tests/scripts/test_branch_claim_check_workflow.py tests/scripts/test_branch_claim.py -q --no-cov"
emergency_bypass:
enabled: false
justification: ""
follow_up_ticket_id: ""
dod_evidence:
- id: "dod-omn20708-ac1-no-markdown-ledger"
description: "With work-ledger the job runs only the validator and the reader, and an unheld branch passes with no ledger present."
source: "manual"
checks:
- check_type: "test_passes"
check_value: "uv run pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -k 'work_ledger_source_runs_no_ledger_step or ledger_absent_passes'"
cwd: "${OMNI_HOME}/omniclaude"
binds_ac: ["AC1"]
- id: "dod-omn20708-ac2-held-elsewhere-refused"
description: "A branch held elsewhere is refused in refuse mode and an unanswered lab check never passes."
source: "manual"
checks:
- check_type: "test_passes"
check_value: "uv run pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -k 'claimed_elsewhere_refused or no_completed_matching_run_fails or garbled_first_line_fails or outcomes_follow_mode'"
cwd: "${OMNI_HOME}/omniclaude"
binds_ac: ["AC2"]
- id: "dod-omn20708-ac3-wired"
description: "The test file is a named step of the Branch Claim Gate job (covered by the required CI Summary context) and a pre-commit hook entry."
source: "manual"
checks:
- check_type: "test_passes"
check_value: "uv run pytest tests/scripts/test_branch_claim_check_workflow.py -q --no-cov -k branch_claim_check_workflow_is_wired"
cwd: "${OMNI_HOME}/omniclaude"
binds_ac: ["AC3"]
Loading
Loading