Skip to content
Closed
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
61 changes: 61 additions & 0 deletions .github/workflows/ci-gates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: ci-gates

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-gates-${{ github.ref }}
cancel-in-progress: true

jobs:
gates:
# Per docs/implementation-plan.md §4.4 — all gates fail-hard, no continue-on-error.
name: ${{ matrix.gate }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
gate:
- loc
- no-stubs
- no-custom-crypto
- no-null-provider-prod
- security-audit-required
- vendored-patch-audit
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so BASE_REF diffs work

- name: Set BASE_REF
id: base
shell: bash
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "BASE_REF=origin/${{ github.base_ref }}" >> "$GITHUB_ENV"
else
echo "BASE_REF=HEAD~1" >> "$GITHUB_ENV"
fi

- name: Make scripts executable
run: chmod +x ci/*.sh

- name: Run gate ${{ matrix.gate }}
shell: bash
env:
VERBOSE: "1"
run: |
case "${{ matrix.gate }}" in
loc) ./ci/check-loc.sh ;;
no-stubs) ./ci/no-stubs.sh ;;
no-custom-crypto) ./ci/no-custom-crypto.sh ;;
no-null-provider-prod) ./ci/no-null-provider-prod.sh ;;
security-audit-required) ./ci/security-audit-required.sh ;;
vendored-patch-audit) ./ci/vendored-patch-audit.sh ;;
esac
188 changes: 188 additions & 0 deletions ci/check-loc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# ci/check-loc.sh — enforces docs/implementation-plan.md §4.2 LOC budget.
#
# Fails if:
# - A budgeted file exceeds its active-phase cap.
# - A budgeted file touched by this PR *grew* vs. BASE_REF.
# - Any non-budgeted file newly added by this PR exceeds the per-language new-file cap.
#
# Overrides: a single line comment `// ci:loc-ok` on the file's first 20 lines
# permits the file to exceed its new-file cap. Budgeted-file caps cannot be
# overridden inline (requires a ci/loc-budget.yaml update, review-gated).
#
# Env:
# BASE_REF — git ref to diff against (default: origin/main).
# VERBOSE=1 — print per-file budget status.
set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"
BUDGET="$REPO_ROOT/ci/loc-budget.yaml"
BASE_REF="${BASE_REF:-origin/main}"

if ! command -v python3 >/dev/null 2>&1; then
echo "check-loc.sh: python3 required" >&2
exit 2
fi

python3 - "$BUDGET" "$BASE_REF" "${VERBOSE:-0}" <<'PY'
import os, re, subprocess, sys, pathlib

budget_path, base_ref, verbose = sys.argv[1], sys.argv[2], sys.argv[3] == "1"
repo_root = pathlib.Path(
subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
)

# -- minimal YAML parse (no PyYAML dep). Handles the shape we control. --
def parse_budget(path):
text = pathlib.Path(path).read_text()
global_cap = {}
files = []
active_phase = "phase0"
current_file = None
in_global = False
in_files = False
for raw in text.splitlines():
line = raw.rstrip()
if not line or line.lstrip().startswith("#"):
continue
if line.startswith("global:"):
in_global, in_files = True, False
continue
if line.startswith("files:"):
in_global, in_files = False, True
continue
if line.startswith("active_phase:"):
active_phase = line.split(":", 1)[1].strip()
continue
if in_global and line.startswith(" ") and ":" in line:
k, v = line.strip().split(":", 1)
v = v.strip().strip('"')
if v.isdigit():
global_cap[k] = int(v)
elif v in ("true", "false"):
global_cap[k] = (v == "true")
else:
global_cap[k] = v
if in_files:
if line.startswith(" - path:"):
if current_file:
files.append(current_file)
current_file = {"path": line.split(":", 1)[1].strip()}
elif current_file is not None and line.startswith(" ") and ":" in line:
k, v = line.strip().split(":", 1)
v = v.strip().strip('"')
if v.isdigit():
current_file[k] = int(v)
else:
current_file[k] = v
if current_file:
files.append(current_file)
return global_cap, files, active_phase

global_cap, files, active_phase = parse_budget(budget_path)
budgeted = {f["path"]: f for f in files}

def run(cmd):
return subprocess.check_output(cmd, text=True, cwd=repo_root).splitlines()

try:
changed = run(["git", "diff", "--name-only", f"{base_ref}...HEAD"])
except subprocess.CalledProcessError:
# new branch without base_ref — fall back to staged + working-tree diff
changed = run(["git", "diff", "--name-only", "HEAD"])
changed += run(["git", "diff", "--name-only", "--cached"])

added = []
try:
added = run(["git", "diff", "--name-only", "--diff-filter=A", f"{base_ref}...HEAD"])
except subprocess.CalledProcessError:
pass

def line_count(path):
p = repo_root / path
if not p.is_file():
return 0
with p.open("rb") as fh:
return sum(1 for _ in fh)

def line_count_at_ref(path, ref):
try:
blob = subprocess.check_output(["git", "show", f"{ref}:{path}"], cwd=repo_root)
return blob.count(b"\n") + (0 if blob.endswith(b"\n") or not blob else 1)
except subprocess.CalledProcessError:
return None # file didn't exist at ref

def has_override(path):
p = repo_root / path
if not p.is_file():
return False
try:
with p.open("r", encoding="utf-8", errors="ignore") as fh:
head = [next(fh, "") for _ in range(20)]
except Exception:
return False
return any(global_cap.get("override_comment", "// ci:loc-ok") in l for l in head)

def phase_cap(entry):
# Walk active_phase down to phase0; return first cap found.
order = ["phase4", "phase3", "phase2", "phase1", "phase0"]
idx = order.index(active_phase) if active_phase in order else len(order) - 1
for p in order[idx:]:
k = f"{p}_cap"
if k in entry:
return entry[k], p
return None, None

def new_file_cap_for(path):
if path.endswith(".rs"):
return global_cap.get("new_file_cap_rust", 800), "rust"
if path.endswith(".ts") or path.endswith(".tsx"):
return global_cap.get("new_file_cap_ts", 800), "ts"
return None, None

failures = []
warnings = []

for path in sorted(set(changed)):
if not path:
continue
if path in budgeted:
entry = budgeted[path]
cap, cap_phase = phase_cap(entry)
current = line_count(path)
if cap is not None and current > cap:
failures.append(
f"{path}: {current} LOC exceeds {cap_phase} cap of {cap} "
f"(baseline {entry.get('baseline_2026_04_24', '?')}). "
f"Decompose before growing."
)
if global_cap.get("touched_must_shrink", False):
base = line_count_at_ref(path, base_ref)
if base is not None and current > base:
failures.append(
f"{path}: grew from {base} -> {current} LOC. "
f"Per §4.3 'touched code pays its decomposition debt' — "
f"touched budgeted files must not grow."
)
if verbose:
print(f"[budget] {path}: {current} / cap {cap} ({cap_phase})")
elif path in added:
cap, lang = new_file_cap_for(path)
if cap is None:
continue
current = line_count(path)
if current > cap and not has_override(path):
failures.append(
f"{path}: new file is {current} LOC, exceeds {lang} cap of {cap}. "
f"Split or add '{global_cap.get('override_comment')}' with reviewer sign-off."
)
elif lang == "ts" and current > global_cap.get("warn_file_cap_ts", 600):
warnings.append(f"{path}: new TS file is {current} LOC (warn threshold {global_cap.get('warn_file_cap_ts')}).")

for w in warnings:
print(f"warn: {w}")
for f in failures:
print(f"fail: {f}", file=sys.stderr)

sys.exit(1 if failures else 0)
PY
95 changes: 95 additions & 0 deletions ci/loc-budget.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# LOC budget — per docs/implementation-plan.md §4.2.
#
# `ci/check-loc.sh` enforces:
# - Every file listed below has a `phase0_cap`; the file must not exceed it.
# - Every not-listed file added in this PR is capped at `new_file_cap`.
# - `touched_must_shrink: true` — a listed file touched by the PR must
# not GROW (line count of new version must be ≤ base-ref line count).
#
# Phase-by-phase caps track the implementation-plan.md §4.2 schedule.
# Baseline captured 2026-04-24 against main (commit d4165da).

global:
new_file_cap_rust: 800
new_file_cap_ts: 800
warn_file_cap_ts: 600
touched_must_shrink: true
override_comment: "// ci:loc-ok"

files:
- path: cli/src/plugin.ts
baseline_2026_04_24: 7455
phase0_cap: 7200
phase1_cap: 6000
phase2_cap: 3000
phase3_cap: 800
notes: "Split into cli/src/core/{session,handoff,offload,mesh,signal,restore}.ts modules."

- path: cli/src/commands/operator.ts
baseline_2026_04_24: 2932
phase0_cap: 2900
phase1_cap: 2000
phase2_cap: 1200
phase3_cap: 800
notes: "Split into cli/src/commands/operator/{tui,input,data,overlays,keymap}.ts."

- path: inference-router/src/handoff.rs
baseline_2026_04_24: 2626
phase0_cap: 2600
phase1_cap: 1800
phase2_cap: 800
notes: "Split client/server/crypto modules."

- path: controller/src/reconciler.rs
baseline_2026_04_24: 2383
phase0_cap: 2350
phase1_cap: 1500
phase2_cap: 800
notes: "Move logic into controller/src/reconcilers/{sandbox,mcp_server,...}.rs."

- path: controller/src/mesh_peer.rs
baseline_2026_04_24: 1970
phase0_cap: 1950
phase1_cap: 1200
phase2_cap: 800
notes: "Pull MeshProvider out; keep mesh_peer.rs as the Vendored impl."

- path: inference-router/src/routes/inference.rs
baseline_2026_04_24: 1866
phase1_cap: 1500
phase2_cap: 800
notes: "Split MCP/A2A handlers into inference-router/src/{mcp,a2a}/."

- path: cli/src/commands/up.ts
baseline_2026_04_24: 1846
phase1_cap: 1500
phase2_cap: 800
notes: "Preflight / provision / helm modules under cli/src/commands/up/."

- path: cli/src/commands/mesh.ts
baseline_2026_04_24: 1583
phase1_cap: 1200
phase2_cap: 800

- path: inference-router/src/routes/handoff.rs
baseline_2026_04_24: 1593
phase1_cap: 1200
phase2_cap: 800

- path: inference-router/src/governance.rs
baseline_2026_04_24: 1252
phase1_cap: 900
phase2_cap: 700
notes: "Becomes pure provider dispatch after full AGT provider landings."

- path: inference-router/src/spawn.rs
baseline_2026_04_24: 1199
phase1_cap: 900
phase2_cap: 800

- path: cli/src/commands/handoff.ts
baseline_2026_04_24: 1119
phase2_cap: 800

# Active phase for the cap lookup. Updated at phase transitions.
active_phase: phase0
Loading