diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9a11e31 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + ci-gate: + name: CI Gate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate JSON schemas + run: python -m json.tool schemas/issue-contract.schema.json >/dev/null && python -m json.tool schemas/pr-contract.schema.json >/dev/null + + - name: Compile flow inspector + run: python -m py_compile scripts/flow/inspect_repo_flow.py diff --git a/docs/autonomous-flow-platform.md b/docs/autonomous-flow-platform.md index b38ac91..6007b5a 100644 --- a/docs/autonomous-flow-platform.md +++ b/docs/autonomous-flow-platform.md @@ -136,11 +136,18 @@ Use GitHub to enforce: - required checks; - no self-approval where possible; - linear history or squash policy; -- auto-merge enabled by default for clean approved PRs; +- PR author enables auto-merge by default where GitHub allows it; - merge queue if repo churn is high enough to keep PRs constantly behind. For Axiom specifically, a GitHub merge queue may be worth testing if `BEHIND` churn keeps destroying flow. +Author-owned auto-merge contract: +- The PR author is responsible for enabling auto-merge as soon as the PR exists and the repo permits it. +- Standard command: `gh pr merge --auto --squash` after the PR body is complete and the branch has been pushed. +- If GitHub refuses auto-merge because the repo/plan/ruleset does not allow it, the author records the exact reason in `## Merge Automation`. +- If auto-merge is unsafe because the PR is human-gated, release-sensitive, or intentionally paused, the author records that reason and Pheidon owns the explicit merge decision. +- A healthy PR with no `autoMergeRequest` and no recorded exception is flow drift; route it back to the author/owning lane for metadata repair, not to John. + ## The flow state machine The autonomous loop should run as a controller, not as independent workers polling randomly. @@ -153,7 +160,7 @@ Controller loop: 5. Dispatch worker if autonomous. 6. Validate worker output. 7. Update GitHub-visible state. -8. Approve/arm auto-merge when gates are met. +8. Verify the PR author armed auto-merge where possible; otherwise record the unavailable/unsafe reason and route fallback merge approval. 9. Close loop and advance next item. Key principle: every item must have either: diff --git a/scripts/flow/inspect_repo_flow.py b/scripts/flow/inspect_repo_flow.py index 62f1495..fe73b74 100755 --- a/scripts/flow/inspect_repo_flow.py +++ b/scripts/flow/inspect_repo_flow.py @@ -154,6 +154,24 @@ def owner_from_author(pr: dict[str, Any]) -> str: return LANE_BY_AUTHOR.get(login, 'Pheidon') +def repair_actor_for_pr(owner_lane: str, checks: dict[str, Any], merge_state: str) -> tuple[str, str]: + failing = {str(name).lower() for name in checks.get('failing') or []} + metadata_markers = ( + 'validate pr description', + 'ci gate', + 'validate secrets', + 'detect relevant changes', + 'attest', + ) + if merge_state in ('BEHIND', 'DIRTY'): + return 'Hephaestus', f'Restore mergeability from {merge_state.lower()} state, rerun required checks, and push with --force-with-lease when rebasing.' + if any(any(marker in name for marker in metadata_markers) for name in failing): + return 'Hephaestus', 'Repair PR metadata, generated governance surfaces, or CI gate wiring, then rerun required checks.' + if owner_lane in ('Pheidon', 'Apollo'): + return 'Daedalus', 'Reproduce failing checks, make the minimal substantive repair, validate, and push.' + return owner_lane, 'Reproduce failing checks, make the minimal repair, validate, and push.' + + def classify_pr(pr: dict[str, Any]) -> dict[str, Any]: checks = check_summary(pr.get('statusCheckRollup') or []) labels = label_names(pr) @@ -173,11 +191,12 @@ def classify_pr(pr: dict[str, Any]) -> dict[str, Any]: if pr.get('autoMergeRequest'): state = 'Auto-merge Armed' next_action = 'Wait for GitHub auto-merge or merge queue completion.' + reasons.append('clean_approved_green') else: - state = 'Ready for Approval' - next_actor = 'Pheidon' - next_action = 'Arm auto-merge after final gate check.' - reasons.append('clean_approved_green') + state = 'Needs Repair' + next_actor = owner_lane + next_action = 'PR author should enable auto-merge where GitHub allows it, or record the unavailable/unsafe reason under Merge Automation.' + reasons.extend(['clean_approved_green', 'auto_merge_missing']) elif changes or review == 'CHANGES_REQUESTED': state = 'Needs Repair' next_actor = owner_lane if owner_lane not in ('Pheidon', 'Apollo') else 'Daedalus' @@ -185,13 +204,13 @@ def classify_pr(pr: dict[str, Any]) -> dict[str, Any]: reasons.append('changes_requested') elif merge_state in ('DIRTY', 'BEHIND'): state = 'Needs Repair' - next_actor = 'Hephaestus' if not checks['failing'] else owner_lane - next_action = f'Restore mergeability from {merge_state.lower()} state, validate, and push.' + next_actor, next_action = repair_actor_for_pr(owner_lane, checks, merge_state) reasons.append(f'merge_state:{merge_state.lower()}') + if checks['failing']: + reasons.append('failing_checks') elif checks['failing']: state = 'Needs Repair' - next_actor = 'Ares' if review == 'APPROVED' else owner_lane - next_action = 'Reproduce failing checks, identify owner, repair or dispatch substantive fix.' + next_actor, next_action = repair_actor_for_pr(owner_lane, checks, merge_state) reasons.append('failing_checks') elif review == 'REVIEW_REQUIRED': state = 'Needs Review'