Skip to content

The CI/PR gate

Everywhere else Vectadyne is advisory. Here it is a gate — and it is a gate because your CI system enforces it, not because Vectadyne reached into your pipeline.

That distinction is the whole design. You opt in, on the repositories you choose, for the actions you choose.

On a pull request: describe the change as an action, get a verdict, fail the check on block or require_approval.

allowwarnblock / require_approvalPull requestCI jobpreflight.actionVerdict check passes passes+ advisory comment check failsreasons + citations

warn passes. It posts the advisory and lets the PR through. A warn that failed a build would be the inline gate this is designed not to be, moved into CI — and your team would learn to ignore the check, which costs you the blocks too.

Use a machine principal, not a person’s token:

name: Vectadyne preflight
on: pull_request
jobs:
preflight:
runs-on: ubuntu-latest
env:
VECTADYNE_MCP_URL: ${{ vars.VECTADYNE_MCP_URL }}
VECTADYNE_CLIENT_ID: ${{ secrets.VECTADYNE_CLIENT_ID }}
VECTADYNE_CLIENT_SECRET: ${{ secrets.VECTADYNE_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v5
with: { fetch-depth: 0 }
- name: Preflight the change
run: ./scripts/vectadyne-preflight.sh

GitHub Actions OIDC is not accepted as a bearer. Exchange the client credential.

The principal needs preflight.action and memory.search. It does not need write capabilities — a gate that can also write to the corpus is a gate that can be talked into rewriting the rule it is enforcing.

import os, subprocess, sys
from vectadyne import Vectadyne, interpret_verdict
changed = subprocess.check_output(
["git", "diff", "--name-only", "origin/main...HEAD"], text=True
).split()
with Vectadyne(mcp_url=os.environ["VECTADYNE_MCP_URL"]) as vd:
assembled = vd.assemble(
action_type="code.apply_patch",
summary=os.environ.get("PR_TITLE", "pull request"),
changed_files=changed,
scope_key=os.environ["VECTADYNE_SCOPE"],
purpose="code_review",
)
d = interpret_verdict(assembled.verdict)
for w in d.warnings:
print(f"::warning::{w}")
if not d.proceed:
print(f"::error::{d.summary()}")
for c in d.cited_memory:
print(f"::error:: cited: {c}")
sys.exit(1)

A failing check that says “blocked” makes you the bottleneck. Post the reasons and citations — which policy, which incident, which prior rollback. Then the author can fix it, or make a reasoned case that the memory is stale, without finding you first.

That second path matters: a block citing a stale_memory_conflict is often the corpus being wrong, and the fix is a correction, not an override.

Run it non-blocking first — report the verdict, never fail the build. Watch for a week or two.

You are looking for false blocks, which mean the corpus has stale or over-broad memory. Fix those before making the check required, or the first week of enforcement will be spent on override requests and the team will conclude the tool is noise.

Not every PR needs this. The action taxonomy exists to let you gate the things that are hard to undo:

  • migrations (data.run_migration)
  • CI config changes (vcs.modify_ci_config)
  • infrastructure (deploy.modify_infra)
  • force-pushes (vcs.force_push)
  • dependency changes (code.modify_dependency)

A gate on everything is a gate people route around.