Skip to content
Merged
63 changes: 63 additions & 0 deletions .github/workflows/hourly-pr-maintenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Hourly PR Maintenance

on:
schedule:
- cron: "11 * * * *"
workflow_dispatch:

concurrency:
group: contextual-orchestrator-hourly-pr-maintenance
cancel-in-progress: false

permissions:
contents: read

jobs:
dispatch-central-review-repair:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch one bounded central review repair
env:
PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}
OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}
run: |
set -euo pipefail
token="${PR_REVIEW_MERGE_TOKEN:-${OPENCODE_APPROVE_TOKEN:-}}"
if [ -z "$token" ]; then
echo "::error::No established central scheduler dispatch credential is configured."
exit 1
fi

payload="$(jq -n \
--arg event_type "pr-review-fix-scheduler" \
'{
"event_type": $event_type,
"client_payload": {
"target_repository": "ContextualWisdomLab/contextual-orchestrator",
"base_branch": "main",
"max_prs": "100",
"max_dispatches": "1",
"retry_hours": "1",
"dry_run": false
}
}')"
response_file="$(mktemp)"
status_code="$(curl \
--silent \
--show-error \
--location \
--output "$response_file" \
--write-out '%{http_code}' \
--request POST \
--header 'Accept: application/vnd.github+json' \
--header "Authorization: Bearer ${token}" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
--data "$payload" \
https://api.github.com/repos/ContextualWisdomLab/.github/dispatches)"
if [ "$status_code" != "204" ]; then
echo "::error::Central review-repair dispatch failed with HTTP ${status_code}."
cat "$response_file"
exit 1
fi
echo "Dispatched one bounded Contextual Orchestrator review-repair opportunity."
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes to this project are documented in this file.
- Structured tool failure categories, stable fallback actions, and public adapter exceptions.
- Secret-free `tool_fallback_decision` audit events.
- Exact regression coverage for the Strix `Tool execute_command not found in agent strix` failure.
- Hourly PR maintenance dispatcher that requests one bounded, exact-target review-repair opportunity from the protected central `.github` control plane.

### Changed

Expand All @@ -20,3 +21,4 @@ All notable changes to this project are documented in this file.

- Ambiguous non-idempotent outcomes, invalid arguments, permission denial, and policy denial fail closed.
- Fallback errors and audit events do not copy provider exception text, tool arguments, outputs, or credentials.
- The hourly caller remains read-only and model-secret-free while preserving exact-head checks, independent approval, and the existing reviewer credential scheme.
69 changes: 69 additions & 0 deletions docs/doctoring/hourly-pr-maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Hourly PR Maintenance Control Boundary

## Decision

Contextual Orchestrator owns a small scheduled caller while the organization
`.github` repository owns the reusable review-repair policy and the OpenCode
writer. The caller sends a `repository_dispatch` event to the protected central
control plane every hour; it does not copy the repair engine into the product
repository.

## Cadence and bounded work

The heartbeat runs at minute 11 of every hour. It asks the central scheduler to
inspect up to 100 open `main` pull requests, dispatch at most one repair, and
enforce a one-hour same-head retry floor. Non-cancelling concurrency avoids
terminating legitimate work merely because the next heartbeat arrived.

The central scheduler performs root-cause analysis and remediation feasibility
screening. Queued checks, reviewer latency, missing independent approval,
provider delay, billing limits, and unavailable credentials are operational
states rather than evidence of a source defect. They must not produce invented
commits.

## Credential, model, and authority boundaries

The caller uses only the established `PR_REVIEW_MERGE_TOKEN`, falling back to
`OPENCODE_APPROVE_TOKEN`, to create the central dispatch event. Its generated
`GITHUB_TOKEN` remains read-only. The caller receives neither
`NVIDIA_NIM_API_KEY` nor any model prompt or source archive. The central,
separately reviewed OpenCode worker owns NVIDIA NIM access and repair execution.
`COPILOT_GITHUB_TOKEN` is not a model, scheduler, review, or merge credential in
this path.

A repair cannot approve itself, weaken checks, label pending evidence as
passing, replay an ambiguous state-changing tool call, or bypass independent
non-author approval. Exact-head CI, security review, coverage, docstrings, and
branch protection remain authoritative.

## Standalone and ecosystem behavior

The product remains independently deployable. The dispatch payload contains
only repository identity, protected base, queue bounds, and retry cadence.
Central `.github` may improve the repair engine without importing product code;
Contextual Orchestrator continues to own provider routing, tool execution,
credential authority, audit behavior, and release evidence.

## Failure handling and rollback

Missing dispatch credentials and non-204 GitHub responses fail closed with an
actionable error. Permanent tests bind the cron, single-flight policy,
one-dispatch limit, one-hour retry floor, exact target, read-only caller
permission, explicit credentials, and absence of model or Copilot secrets.
Rollback removes the caller, test, and this record; it does not change reviewer
identities, central worker secrets, branch protection, or product runtime
behavior.

## APA 7th references

GitHub, Inc. (n.d.-a). *Events that trigger workflows*. GitHub Docs. Retrieved
August 15, 2026, from
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows

GitHub, Inc. (n.d.-b). *REST API endpoints for repositories: Create a repository
dispatch event*. GitHub Docs. Retrieved August 15, 2026, from
https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event

GitHub, Inc. (n.d.-c). *Workflow syntax for GitHub Actions: Permissions*.
GitHub Docs. Retrieved August 15, 2026, from
https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions
82 changes: 82 additions & 0 deletions tests/test_hourly_pr_maintenance_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Contract tests for the bounded hourly PR-maintenance dispatcher."""

from pathlib import Path


WORKFLOW = Path(".github/workflows/hourly-pr-maintenance.yml")
DOCTORING = Path("docs/doctoring/hourly-pr-maintenance.md")
CHANGELOG = Path("CHANGELOG.md")


def _read(path: Path) -> str:
"""Read one repository contract file as UTF-8 text."""
return path.read_text(encoding="utf-8")


def test_hourly_workflow_is_bounded_and_non_cancelling() -> None:
"""Schedule one bounded heartbeat without cancelling prior legitimate work."""
workflow = _read(WORKFLOW)

for expected in (
' - cron: "11 * * * *"',
" workflow_dispatch:",
" group: contextual-orchestrator-hourly-pr-maintenance",
" cancel-in-progress: false",
" contents: read",
" timeout-minutes: 5",
):
assert expected in workflow
assert workflow.count(" schedule:") == 1
assert workflow.count(" workflow_dispatch:") == 1
assert workflow.count(" cancel-in-progress: false") == 1


def test_dispatch_targets_central_policy_with_bounded_inputs() -> None:
"""Delegate policy to central .github and limit each heartbeat to one repair."""
workflow = _read(WORKFLOW)

for expected in (
'"target_repository": "ContextualWisdomLab/contextual-orchestrator"',
'"base_branch": "main"',
'"max_prs": "100"',
'"max_dispatches": "1"',
'"retry_hours": "1"',
'"event_type": $event_type',
"https://api.github.com/repos/ContextualWisdomLab/.github/dispatches",
):
assert expected in workflow
assert '"dry_run": false' in workflow
assert 'status_code" != "204"' in workflow


def test_dispatch_preserves_model_and_credential_boundaries() -> None:
"""Keep model keys out of the caller and preserve the established review tokens."""
workflow = _read(WORKFLOW)

assert "PR_REVIEW_MERGE_TOKEN" in workflow
assert "OPENCODE_APPROVE_TOKEN" in workflow
assert "NVIDIA_NIM_API_KEY" not in workflow
assert "COPILOT_GITHUB_TOKEN" not in workflow
assert "secrets: inherit" not in workflow
assert "contents: write" not in workflow
assert "pull-requests: write" not in workflow
assert "issues: write" not in workflow


def test_doctoring_and_changelog_define_operating_contract() -> None:
"""Keep cadence, authority, failure handling, and references auditable."""
doctoring = _read(DOCTORING)
changelog = _read(CHANGELOG)

for phrase in (
"root-cause analysis",
"remediation feasibility",
"one-hour same-head retry floor",
"NVIDIA_NIM_API_KEY",
"COPILOT_GITHUB_TOKEN",
"independent non-author approval",
"repository_dispatch",
"APA 7th references",
):
assert phrase in doctoring
assert "Hourly PR maintenance dispatcher" in changelog