Skip to content

Latest commit

 

History

History
443 lines (337 loc) · 23 KB

File metadata and controls

443 lines (337 loc) · 23 KB

CI integration

CI integration is the most consequential structural change agent-redline introduces into a repo. It affects every developer, every change to a long-lived branch, and the repo's branch protection (when applicable). Bootstrap mode never auto-commits CI changes. The skill produces a proposal; humans decide.

This document is what the proposal generated by bootstrap looks like, and how to apply it.

Principle

CI is red-zone. The skill respects its own discipline.

Two flow modes

agent-redline supports two CI flow modes; bootstrap picks one based on Phase 1 inspection (see BOOTSTRAP.md) and the developer's confirmation:

Flow mode When Trigger Verdict surface Workflow gate
PR-driven Team flow with PR review on: pull_request: Sticky comment on the PR Fails on exit 2 (binding-mode hard fail)
Push-driven Solo / trunk-based / no PR review on: push: branches: [main] $GITHUB_STEP_SUMMARY on the run page; the workflow-failure email lands the reviewer there in one click Fails on EXIT != 0 (both RED warnings and BOUNDARY_VIOLATION hard fails) so GitHub's default workflow-failure email fires. agent-redline ships as its own .github/workflows/ file — its failure does not affect other workflows in the repo.

The reporter exit-code contract is the same in both modes: 0 clean, 1 warnings (gray-zone, watch-list touched, unmet checkpoint in shadow, PR-size warn), 2 binding-mode hard fail (boundary violation, unsatisfied checkpoint under binding, PR-size fail under binding). The two modes differ only in trigger, in how they compute the changed-files diff, and in how the enforce step gates CI.

Most repos are PR-driven. Solo developers and trunk-based teams are push-driven; the bootstrap detection signal is "does the existing repo open PRs, or is its history dominated by direct pushes to a long-lived branch?"

What the bootstrap proposal contains

After bootstrap, you have a file at docs/agent-redline-ci-proposal.md containing:

  1. A workflow file ready to copy into .github/workflows/agent-redline.yml — shaped for the chosen flow mode
  2. Required-status-check additions to add to branch protection (PR-driven only; push-driven repos that don't gate via a separate review step skip this)
  3. CODEOWNERS additions, mapped best-effort to your team structure (PR-driven only; push-driven solo repos skip)
  4. Initial mode recommendation — always shadow
  5. Timeline guidance — 4 weeks or 30 changesets of shadow before flipping checks to binding (a "changeset" is a merged PR or a single commit, depending on flow)
  6. Decisions flagged for human judgment:
    • Who owns each checkpoint? (PR-driven only)
    • Which checks should eventually become binding?
    • Are there platform-team policies that override anything here?

The proposed workflow — PR-driven

Used when the repo's dominant flow is PR-based.

name: agent-redline

on:
  pull_request:
    types: [opened, synchronize, reopened, edited, labeled, unlabeled]

permissions:
  contents: read
  pull-requests: write   # for the sticky verdict comment

jobs:
  boundary:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Stack-specific install + boundary-backend invocation.
      # Spring (ArchUnit):
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21' }
      - run: ./gradlew test --tests '*ArchitectureTest'
        # Backend exits non-zero on violations; the reporter surfaces them.
        # Drop continue-on-error: once shadow-window calibration is done.
        continue-on-error: true
      - uses: actions/upload-artifact@v4
        with: { name: boundary-report, path: build/test-results/test/ }

  report:
    needs: boundary
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/download-artifact@v4
        with: { name: boundary-report, path: build/ }
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      - run: pip install pyyaml jsonschema

      - name: Run reporter
        id: report
        # Capture the reporter's exit code; do NOT let bash -e propagate
        # it. The sticky-comment + enforce steps must run regardless.
        run: |
          set +e
          mkdir -p build
          git diff --name-only \
            ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
            > build/changed-files.txt
          # `--numstat` per-file line counts so the reporter can apply
          # policy.excludes to the prSize check (without it, excludes
          # affect zone classification but not size).
          git diff --numstat \
            ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
            > build/lines-per-file.txt
          # `--unified=0` raw patch so the reporter can scan added lines
          # for suppression markers (`# noqa`, `@SuppressWarnings`,
          # backend-allowlist edits, etc.). Only consumed when the policy
          # declares a `suppressions:` block; otherwise the reporter
          # ignores the file. See "Suppression detection" below.
          git diff --unified=0 \
            ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
            > build/diff-unified.patch
          LABELS=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH" | paste -sd,)
          python scripts/agent-redline-report.py \
            --policy agent-policy.yaml \
            --changed-files build/changed-files.txt \
            --lines-per-file build/lines-per-file.txt \
            --diff-unified build/diff-unified.patch \
            --pr-labels "$LABELS" \
            --json-out build/verdict.json \
            --comment-out build/comment.md
          EXIT=$?
          echo "exit_code=$EXIT" >> "$GITHUB_OUTPUT"

      - name: Post sticky PR comment
        uses: marocchino/sticky-pull-request-comment@v2
        with: { path: build/comment.md, header: agent-redline }

      - name: Enforce reporter exit code
        # Fail CI only on exit 2 (binding-mode hard fail). Exit 1 surfaces
        # in the sticky comment without blocking merge.
        run: |
          EXIT="${{ steps.report.outputs.exit_code }}"
          if [[ "$EXIT" == "2" ]]; then
            echo "Reporter exited 2 (binding-mode hard fail). Failing the report check."
            exit 1
          fi

Replace the Spring setup-java + gradlew lines with the matching steps for your stack — pip install -e '.[dev]' && python scripts/run-import-linter.py --out build/import-linter-report.json for Python, etc. The boundary job's contents are stack-specific; the report job is stack-neutral.

The proposed workflow — push-driven

Used when the dominant flow is git push to a long-lived branch and PRs are rare or absent.

name: agent-redline

on:
  push:
    branches: [main]   # or whichever long-lived branch you push to

permissions:
  contents: read

jobs:
  boundary:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ... same boundary-backend invocation as PR-driven (see above) ...
      - uses: actions/upload-artifact@v4
        with: { name: boundary-report, path: build/import-linter-report.json }

  report:
    needs: boundary
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/download-artifact@v4
        with: { name: boundary-report, path: build/ }
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      - run: pip install pyyaml jsonschema

      - name: Run reporter
        id: report
        # BEFORE/AFTER diff with merge-base fallback for first-push and
        # force-push edge cases (where github.event.before is all-zeros
        # or a SHA the runner doesn't have). --flow-mode push tells the
        # reporter to render checkpoint text as a review obligation on
        # the commit (CODEOWNER / label phrasing doesn't apply on push).
        run: |
          set +e
          mkdir -p build
          BEFORE="${{ github.event.before }}"
          AFTER="${{ github.sha }}"
          if [[ "$BEFORE" == "0000000000000000000000000000000000000000" || -z "$BEFORE" ]] || \
             ! git rev-parse --verify "$BEFORE^{commit}" >/dev/null 2>&1; then
            BEFORE="$(git merge-base origin/main "$AFTER" 2>/dev/null || echo "$AFTER^")"
          fi
          git diff --name-only "$BEFORE"..."$AFTER" > build/changed-files.txt
          # Per-file line counts so policy.excludes applies to prSize.
          git diff --numstat "$BEFORE"..."$AFTER" > build/lines-per-file.txt
          # Raw `--unified=0` patch for suppression-marker detection (see
          # "Suppression detection" below). Reporter ignores the file
          # unless the policy declares a `suppressions:` block.
          git diff --unified=0 "$BEFORE"..."$AFTER" > build/diff-unified.patch
          python scripts/agent-redline-report.py \
            --policy agent-policy.yaml \
            --flow-mode push \
            --changed-files build/changed-files.txt \
            --lines-per-file build/lines-per-file.txt \
            --diff-unified build/diff-unified.patch \
            --json-out build/verdict.json \
            --comment-out build/comment.md
          EXIT=$?
          echo "exit_code=$EXIT" >> "$GITHUB_OUTPUT"

      - uses: actions/upload-artifact@v4
        with:
          name: agent-redline-verdict
          path: |
            build/verdict.json
            build/comment.md

      # Append the verdict to $GITHUB_STEP_SUMMARY so it surfaces at
      # the top of the run page (one click from the workflow-failure
      # email). See extensions/python/scaffold.md §5b for the full
      # canonical pattern.

      - name: Enforce reporter exit code
        # Push-mode: fail the agent-redline workflow on EXIT != 0 (both
        # RED warnings and BOUNDARY_VIOLATION hard fails). The red badge
        # on this commit's agent-redline run is the audit record;
        # GitHub's default email-on-failure summons the reviewer; other
        # workflows in this repo run independently and are unaffected.
        run: |
          EXIT="${{ steps.report.outputs.exit_code }}"
          if [[ "$EXIT" != "0" ]]; then
            echo "Reporter exited $EXIT. Failing the agent-redline workflow."
            exit 1
          fi
          echo "Reporter exited 0 — clean."

The differences from PR-driven, summarized:

  • Trigger: on: push: instead of on: pull_request:
  • No pull-requests: write permission (no PR to comment on)
  • Diff: ${{ github.event.before }}...${{ github.sha }} with merge-base fallback
  • --flow-mode push flag so the reporter renders checkpoint text as a review obligation (no CODEOWNER / label phrasing)
  • No --pr-labels (no PR has labels)
  • No sticky-comment step; verdict surfaces via $GITHUB_STEP_SUMMARY on the run page
  • Verdict also uploaded as a CI artifact
  • Enforce step gates on EXIT != 0, so GitHub's default workflow-failure email fires for both RED and BOUNDARY_VIOLATION
  • agent-redline ships as its own .github/workflows/ file; failing it does not affect other workflows in the repo

Suppression detection

When the consuming repo's agent-policy.yaml declares a suppressions: block, the reporter scans build/diff-unified.patch for added-line suppression markers — inline comments (# noqa, # type: ignore, // archunit: ignore), annotations (@SuppressWarnings, @ArchIgnore), and structural edits to declared backend-config keys (ignore_imports in pyproject.toml, per-file-ignores in setup.cfg, etc.). Markers added on guarded surfaces escalate the verdict to RED and route to the architecture-review checkpoint, which is the same human-attention mechanism red-zone changes already use.

The marker list is per-stack and ships with the language extension (see EXTENSIONS.mdsuppressions.yaml). Bootstrap vendors it to .agent-redline/suppressions.yaml in the consuming repo; the reporter reads only that vendored copy. Repos that don't declare a suppressions: block in their policy keep working unchanged — the --diff-unified input is harmless when detection is OFF.

The escalation is hardcoded binding by default — symmetric with boundary_violation. modes.default: shadow does not downgrade it; only an explicit modes.perCheck.suppression: shadow flips it. Full design rationale (including why the naive added-line scan over per-position pairing) is in superpowers/specs/2026-06-10-suppression-detection-design.md; the decision record is in DECISIONS.md under "2026-06-11 — Suppression detection".

Boundary-backend baseline

Adding a boundary-rule backend (ArchUnit on JVM, dependency-cruiser on Node, import-linter on Python, Semgrep, etc.) to a repo that's been developed without it usually surfaces existing violations. Treating those as binding-from-day-one would block legitimate work and the team would rip the check out within a week. The honest pattern:

  • Run the backend once on main. Capture the current set of violations as a baseline (e.g., import-linter's ignore_imports entries; ArchUnit's frozen-baseline pattern).
  • Binding for new violations only. New violations introduced by a change fail CI. Pre-existing baseline violations are reported but don't block.
  • Shrink the baseline over time. Each time a baseline violation is fixed, remove it. The file should never grow.

Some rules genuinely have zero violations on main from day one (typically rules added when the layout was created, or trivially-satisfied ones). Those can be binding immediately. The baseline pattern is for rules retrofitted onto existing code.

The bootstrap proposal calls out which rules likely need the baseline pattern based on a quick dry-run during inspection.

Required status checks (PR-driven only)

After the workflow has run on at least one PR, consider adding these as required status checks in branch protection:

  • agent-redline / boundary — required for new violations; pre-existing baseline violations don't block.
  • agent-redline / report — shadow first, required after tuning.

This requires repo-admin permissions. Push-driven setups skip required-status-checks (CI red on the long-lived branch is the signal regardless of whether the check is "required").

CODEOWNERS additions (PR-driven only)

Bootstrap proposes additions like:

# agent-redline checkpoint routing
/src/main/java/**/domain/**           @org/architecture-team
/src/main/java/**/application/**      @org/architecture-team
/openapi/**                           @org/api-owners
/src/main/resources/db/migration/**   @org/data-owners
/src/main/java/**/security/**         @org/security-team

The team names are best-effort guesses. The developer must replace them with the real teams. Push-driven solo repos don't need CODEOWNERS.

Composing with existing review agents (PR-driven only)

Many shops already run review-style agents (an architect, a security reviewer, a QA pass) over PRs. agent-redline composes with them; it doesn't replace them.

The agent-redline checkpoint defines what needs to happen. The review agent is one mechanism that can satisfy it. The gate is whichever the policy declares — codeownerApproval or a label.

Two common shapes:

Shape A — review agent is a CODEOWNER

The reviewer posts an approval as a CODEOWNER for the affected paths. The checkpoint lists codeownerApproval and the existing CODEOWNERS routes red-zone paths to the team or bot account the agent acts as.

checkpoints:
  architecture-review:
    satisfiedBy:
      - codeownerApproval

The architect agent's prompt needs to know it's the satisfaction signal: when invoked on a red-zone PR, its job is to either approve as the team it represents (which CODEOWNERS routes through) or refuse with a reason.

Shape B — review agent applies a label

The reviewer applies a named label after passing its review. The checkpoint lists label: <name>.

checkpoints:
  architecture-review:
    satisfiedBy:
      - codeownerApproval
      - label: architecture-reviewed

Some shops prefer labels because the audit trail is clearer; others prefer CODEOWNERS because the routing is already in place.

Wiring it up

  1. Decide which checkpoints in your policy are reviewed by which agent.
  2. For each, pick Shape A (CODEOWNER) or Shape B (label).
  3. Update CODEOWNERS or add the label-application instruction to the review agent's prompt.
  4. Run shadow mode for a few weeks. Watch for: checkpoints that never get satisfied (agent doesn't know it should approve / label), and checkpoints that get satisfied without review (CODEOWNERS routes too broadly).

What agent-redline does NOT do:

  • Tell the review agent how to review.
  • Parse the review agent's output to decide whether the checkpoint is satisfied. The PR's labels and approvals are the signal.
  • Coordinate multiple review agents.

Modes: shadow vs binding

Shadow

The check runs and reports. PR-driven posts the sticky comment; push-driven uploads the artifact. Neither blocks merge / fails CI for warnings (exit 1) — only binding-mode hard fails (exit 2) can block.

This is the correct starting mode. It produces real data without disrupting the team.

Binding

The check runs and blocks merge / fails CI on rule violations. The verdict surface (comment or artifact) is the same; the consequence is different.

See "Tuning during shadow — two distinct decisions" below for when and how to flip.

Other CI systems

The reporter ships as a standalone script, runnable from any CI system.

# GitLab CI sketch (uses the standalone reporter)
agent-redline:
  image: python:3.12
  script:
    - python scripts/agent-redline-report.py
        --policy agent-policy.yaml
        --changed-files <generated-list>
        --default-mode shadow
// Jenkins sketch
stage('agent-redline') {
  steps {
    sh '''
      python scripts/agent-redline-report.py \
        --policy agent-policy.yaml \
        --changed-files <generated-list> \
        --default-mode shadow
    '''
  }
}

The reusable GitHub Action wrapping the reporter is on the roadmap; it does not exist yet. CI invocations call the standalone script directly.

What the proposal does NOT do

  • Does not modify branch protection. That requires admin permission and human judgment about which checks should be required.
  • Does not configure the GitHub App or bot identity that posts the PR comment. The default uses GITHUB_TOKEN; for cross-fork updates, a separate App may be wanted.
  • Does not delete or replace existing workflows. It adds a new workflow file.
  • Does not assume any particular team structure. CODEOWNERS additions are placeholders.

Local mirror

The local pre-push check (scripts/agent-redline-check.sh) runs the same reporter on the local diff. Operating mode invokes it before declaring work complete. This closes the "passes locally, fails CI" loop, and also gives push-driven solo developers a verdict signal at edit time without depending on CI.

./scripts/agent-redline-check.sh                   # against origin/main
./scripts/agent-redline-check.sh --base develop    # against another base

Tuning during shadow — two distinct decisions

Shadow mode answers two separate questions, and treating them as one is a common mistake.

Window 1 — Zone calibration (the first 1-2 weeks)

Question: did the bootstrap-time calibration match reality?

Bootstrap (core/skill/bootstrap-mode.md Phase 3b) tunes the policy against the last 30 merged PRs OR the last 30 commits on the long-lived branch (push-driven flow), depending on which signal is available. Window 1 confirms that tuning under live conditions and catches anything bootstrap couldn't see. If the repo had thin history at bootstrap (<30 changesets either way), Window 1 is the first pass at calibration — plan for 3–4 weeks rather than 1–2.

The signal is firing rate per red entry:

  • Red entry firing on 80%+ of changesets → too broad. Downgrade to watch (still surfaced) or blue (autonomous).
  • Red entry firing on 30-50% → ambiguous. Try to split it (interfaces vs implementations, prod config vs all config). If it can't be split, leave it and re-evaluate after Window 2.
  • Red entry firing on <15% → probably right.

Use scripts/agent-redline-tune.py to compute firing rates without waiting for live traffic:

  • PR-driven: python <skill-root>/scripts/agent-redline-tune.py --policy agent-policy.yaml --repo <gh-slug> --limit 30 --suggest
  • Push-driven: python <skill-root>/scripts/agent-redline-tune.py --policy agent-policy.yaml --push-history --branch main --limit 30 --suggest

Re-tuning the policy is normal during this window. The policy is data, edited like any other repo file.

A note on path-based vs. semantic triggers. When tuning, prefer semantic / diff-based signals over path-based ones where the signal exists. The api: openapi-from-controllers diff identifies actual contract changes; matching **/*Controller.java does not (it fires on bug-fixes and refactors too). The schema-detect signal identifies actual migrations. Use path-based red zones for cases where no semantic signal is available — security, infra-as-code, the architecture-test files themselves.

Window 2 — Check-flip tuning (after zones stabilize)

Question: which rules are ready to enforce?

Once the zones are settled, decide which modes.perCheck rules should flip from shadow to binding:

  1. boundary_violation — defaults to binding already; that's the right setting once the boundary baseline is in place. Pre-existing baseline violations don't block; new violations do.
  2. pr_size — flip when the team's normal change shape comfortably fits the fail threshold. Most likely to fight existing reality, so flip last.
  3. report — controls whether unmet required checkpoints fail the check. Flip once checkpoint owners (CODEOWNERS, label-applying agents/humans) are reliably wired up. Until then, leave shadow so missing labels don't block legitimate merges.

Flip one rule at a time. After each flip, watch for a week. If false positives appear, tune the policy and re-shadow that rule before re-flipping.

Why the order matters

Flipping rules to binding before zones are calibrated produces guaranteed alert fatigue: the team gets blocked on changes the policy mis-classified, and they correctly conclude the tool is broken. Zone calibration first; check-flip second.

When to skip CI integration entirely

Some repos are fine with just the local pre-push check and the agent-side discipline. Skipping CI is a reasonable choice if:

  • The repo has very few changes
  • The team is small and reviewers know the codebase deeply
  • A boundary-rule backend (e.g., ArchUnit, import-linter) already runs as part of normal CI
  • You're a solo developer who runs ./scripts/agent-redline-check.sh before every push

In that case, bootstrap still produces the proposal; you just don't apply it. The skill doesn't insist.