diff --git a/config/README.md b/config/README.md index 0af7bf3d1..fe4a7755c 100644 --- a/config/README.md +++ b/config/README.md @@ -193,6 +193,11 @@ repo_settings: Each check has: - `name`: Display label (e.g., "lint", "test", "integration") - `command`: Shell command to execute +- `fix` (optional): Single shell command string that auto-remediates a failing check (e.g. `make lint-fix` for a `lint` check). It must be a *string*: a YAML list is silently coerced to its Python repr (`"['make fmt', 'make lint-fix']"`) and will never run, so chain steps with `&&` or point at a make target; an empty or omitted value (a bare `fix:`, `fix: ""`, `fix: false`, `fix: 0`) is dropped entirely, leaving the check with no fix and a block message that never mentions `fix`. When the per-slice green gate finds this check red at the slice tip, it runs the fix and re-runs the check. It commits and pushes the result to the slice integration branch only when **all** of the following hold (#3409): every *genuine* red check's fix turned it green (reds tagged with an infra signature are excluded before this decision under the default `EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=on`, #3417 — set that switch to `off` and every red, infra-tagged or not, must carry a fix that re-ran green); a final full re-run of every check the gate runs against the all-fixes-applied tree is also green; the runner's tree gained no new non-ignored untracked files, whether from the fix *or* from the checks themselves — the baseline is captured before the first check runs (the gate stages with `git add -u`, which would silently drop them; gitignored droppings don't count); at least one tracked file in that tree is modified, so there is something to commit; the runner reported a final-verification verdict at all and could determine that untracked count (a best-effort git failure in the runner is treated as unsafe); and the gate is running in `on` mode. Every refusal is logged as `autofix_block_reason` on the orchestrator's `Green gate red` line — a `log`-mode refusal is not one of them (that line reads "autofix available but not applied" and carries no such field) — and the reason reaches the slice failure message only when every genuine red's own re-run went green; otherwise it stays in the orchestrator's structured log. Checks without `fix`, or whose re-run stays red, route red verdicts back to the slice team unchanged. When *every* red is infra-tagged the gate fails open and returns **before** this decision, so even a fix that re-ran green is discarded with the worktree rather than committed. + + **`on` is the default mode — no opt-in is required.** `EGG_SLICE_GREEN_GATE` is unset in the shipped configuration, and both an unset and an unrecognised value resolve to `on`, so autofix commits and pushes to your slice integration branch unless you turn it down. Set `EGG_SLICE_GREEN_GATE=log` to keep the fix running as a soak signal while committing and pushing nothing, or `off` to stop the gate before it spawns the runner at all — under `off` the fix never executes. + + `fix` is used only by the per-slice green gate (slice-DAG implement pipelines); the tester step ignores it, so setting it on a non-slice pipeline has no effect. It is also a silent no-op on a check the gate skips: `EGG_SLICE_GREEN_GATE_SKIP_CHECKS` excludes checks by name before the gate runs, and defaults to `security`. See [slice-dag.md](../docs/architecture/slice-dag.md) for the `EGG_SLICE_GREEN_GATE` rollout switch. Checks run sequentially during the implement phase tester step. If not configured, the tester falls back to auto-discovery (scanning for Makefile, package.json, pyproject.toml, etc.). diff --git a/config/repo_config.py b/config/repo_config.py index 5343a1a70..24ddc20df 100644 --- a/config/repo_config.py +++ b/config/repo_config.py @@ -577,7 +577,7 @@ def get_repo_checks(repo: str) -> list[dict[str, str]]: """Get configured check commands for a repository. These are the commands to run during the SDLC pipeline implement phase - checker step. Each check has a "name" (display label) and "command" + tester step. Each check has a "name" (display label) and "command" (shell command to execute). They run sequentially. A check may also carry an optional "fix" command — an auto-remediation the per-slice green gate runs when the check is red at the slice tip (#3409). diff --git a/config/repositories.yaml.example b/config/repositories.yaml.example index d1d6063ec..3701d9b4c 100644 --- a/config/repositories.yaml.example +++ b/config/repositories.yaml.example @@ -75,15 +75,53 @@ readable_repos: # "bot" (default): Use GitHub App bot identity # "user": Use personal access token with user identity # - checks: List of check commands for the SDLC pipeline implement phase -# Each entry has "name" (display label) and "command" (shell command) -# These run sequentially during the checker step -# An entry may also set "fix" (shell command): when the per-slice green -# gate finds that check red at the slice tip, it runs the fix command, -# re-runs the check, and — if the fix turned it green — commits and -# pushes the result to the slice integration branch as the orchestrator -# (#3409). Only configure deterministic auto-remediations here, e.g. -# "make lint-fix" for a format/lint check. Checks without "fix" route -# red verdicts back to the slice team unchanged. +# Each entry has "name" (display label), "command" (shell command) and +# an optional "fix" (single shell command string — a YAML list is +# silently coerced to its Python repr and will never run, so chain +# steps with "&&" or point at a make target; an empty or omitted +# value — a bare "fix:", "", false, 0 — is dropped entirely, +# leaving the check with no fix and a block message that never +# mentions "fix") +# These run sequentially during the tester step +# "fix" auto-remediates that check at the per-slice green gate: when the +# gate finds the check red at the slice tip it runs the fix command and +# re-runs the check. It commits and pushes the result to the slice +# integration branch as the orchestrator only when ALL of the following +# hold (#3409): every *genuine* red check's fix turned it green (reds +# tagged with an infra signature are excluded first under the default +# EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=on, #3417 — set it "off" and +# every red must carry a fix that re-ran green); a final full re-run of +# every check the gate runs against the all-fixes-applied tree is also +# green; the runner's tree gained no new non-ignored untracked files, +# from the fix or from the checks themselves (the baseline is taken +# before the first check runs; the gate stages with "git add -u", which +# would silently drop them); at least one tracked file in that tree is +# modified, so there is something to commit; the runner reported a +# final-verification verdict at all and could determine that untracked +# count (a best-effort git failure in the runner is treated as unsafe); +# and the gate is running in "on" mode. Every refusal is logged as +# autofix_block_reason on the orchestrator's "Green gate red" line — +# a "log"-mode refusal is not one of them (that line reads "autofix +# available but not applied" and carries no such field) — and the +# reason reaches the slice failure message only when every genuine +# red's own re-run went green; otherwise it stays in the structured +# log. When EVERY red is infra-tagged the gate fails +# open and returns BEFORE this decision, so even a fix that re-ran +# green is discarded with the worktree rather than committed. +# NOTE: "on" is the DEFAULT mode — no opt-in is required. Nothing in +# the shipped configuration sets EGG_SLICE_GREEN_GATE, and both an +# unset and an unrecognised value resolve to "on", so autofix commits +# and pushes to your slice integration branch unless you turn it down. +# Set EGG_SLICE_GREEN_GATE=log to keep the fix running as a soak signal +# while committing and pushing nothing, or "off" to stop the gate +# before it spawns the runner at all — under "off" the fix never runs. +# Only configure deterministic auto-remediations here, e.g. +# "make lint-fix" for a format/lint check. Checks without "fix", or +# whose re-run stays red, route red verdicts back to the slice team +# unchanged. "fix" is green-gate-only (slice-DAG implement pipelines): +# the tester step ignores it, and it never runs on a check excluded by +# EGG_SLICE_GREEN_GATE_SKIP_CHECKS (default: "security"). +# See docs/architecture/slice-dag.md for the rollout switch. # - build_commands: Commands to run during Docker image build to install # project-specific dependencies. Results are baked into the image so # containers start with dependencies pre-installed (critical for private diff --git a/docs/architecture/slice-dag.md b/docs/architecture/slice-dag.md index 9be75cd4a..a9ded78d1 100644 --- a/docs/architecture/slice-dag.md +++ b/docs/architecture/slice-dag.md @@ -554,27 +554,67 @@ shape: repo's configured checks at the integration-branch tip; staged rollout via `EGG_SLICE_GREEN_GATE` — default `on` (a red verdict withholds the slice PR), `log` runs the checks and logs the - verdict without blocking; fail-open on infra errors, including - infra-signature-tagged reds inside check execution, #3417. - **The gate can also write to the integration branch**: when every - genuine red carries an optional `fix:` command in - `repositories.yaml` (e.g. `lint: {fix: make lint-fix}`), the - runner applies the fixes in its worktree and the orchestrator - commits them as `egg-green-gate` and pushes to the integration - branch via the launcher-authed gateway push route, #3409. This is - the one place the orchestrator authors commits on a slice branch; - it fires only in `on` mode, and only when the runner proves the - exact tree `git add -u` will stage is green — one full re-run of - *every* configured check against the all-fixes-applied tree - (`final_verification.all_ok`) plus a no-new-untracked-files - check. Any failure to commit or push blocks the slice exactly - like an unfixed red) — calls + verdict without blocking, `off` returns before the runner Job is + spawned at all; fail-open on infra errors, including + infra-signature-tagged reds inside check execution, #3417; the + gate can also write to the integration branch — see + "Green-gate autofix" below) — calls `GatewayClient.create_slice_pr` with `base` resolved from the slice's DAG parent (root → latest completed chain tip, else the pipeline branch (#3541); child → parent's integration branch). On failure the worker calls `scheduler.record_failure(slice_id)`, which arms the cascade timer. + + **Green-gate autofix (Stage A, #3409)** — the one place the + orchestrator authors commits on a slice branch. When every + genuine red carries an optional `fix:` command in + `repositories.yaml`, the runner applies the fixes in its + worktree and the orchestrator commits them as `egg-green-gate` + and pushes to the integration branch via the launcher-authed + gateway push route. `fix:` is a third key on a `checks:` list + entry — `- {name: lint, command: make lint, fix: make lint-fix}` + — not a name-keyed mapping; `validate_checks` drops any entry + missing `name`/`command` with no warning, and a repo whose + `checks:` all drop out gets no green gate at all, not merely no + autofix. Reds tagged with an infra signature are excluded from + "genuine" only under + the default `EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=on`; set it + `off` and every red must carry a fix that re-ran green. When + *every* red is infra-tagged the gate fails open and returns + **before** the autofix decision, so a fix that re-ran green is + discarded with the worktree rather than committed — deliberately, + since the run that proved it was already classified as + untrustworthy. In the mixed case the commit message names only + the genuine reds (`fixed_checks=genuine_failed`) even though the + committed tree also carries any infra-tagged check's fix. + + Autofix fires only in `on` mode — which is the *default*, not an + opt-in: `EGG_SLICE_GREEN_GATE` is unset in the shipped + configuration and both an unset and an unrecognised value + resolve to `on`. It fires only when the runner proves the exact + tree `git add -u` will stage is green — one full re-run of + *every check the gate runs* against the all-fixes-applied tree + (`final_verification.all_ok`; + `EGG_SLICE_GREEN_GATE_SKIP_CHECKS` drops names before the + runner sees them, default `security`), plus a + no-new-*non-ignored*-untracked-files check + (`git ls-files --others --exclude-standard`, so gitignored + droppings don't count) over the runner's whole session rather + than the fix alone, plus at least one tracked modification for + `git add -u` to stage — the gate refuses when the re-runs went + green but staged nothing. `_autofix_ready` also refuses when the + runner reported no final-verification verdict at all + (`final_verification.ran` false) or could not determine the + untracked count (`new_untracked_count is None`, a best-effort + git failure); every refusal is logged as `autofix_block_reason` + on the orchestrator's `Green gate red` line — a `log`-mode + refusal is not one of them, it logs "autofix available but not + applied" with no such field — and the reason reaches the + operator-facing slice failure message only when every genuine + red's own re-run went green; otherwise it stays in the + structured log. Any failure to commit or push blocks the + slice exactly like an unfixed red. 4. After the wave completes, `scheduler.poll_cascades()` drains any expired cascades and emits the orchestrator-side `OVERSEER_ALERT` for each (see "Failure cascade"). diff --git a/docs/development/STRUCTURE.md b/docs/development/STRUCTURE.md index 37f92fbc8..ad4da7d48 100644 --- a/docs/development/STRUCTURE.md +++ b/docs/development/STRUCTURE.md @@ -110,7 +110,7 @@ orchestrator/ ├── slice_scheduler.py # Wave-based scheduler for the implement-phase slice DAG: computes execution waves, caps concurrency, two-tier max_cycles accounting, failure-cascade detection (#2137) ├── stacked_pr_reconciler.py # Stacked-PR rebase reconciler: detects child slice PRs whose base branch was deleted after a parent merge and retargets them via gateway rebase_onto (#2137) ├── cross_repo_merge_gate.py # Cross-repo merge-sequencing gate for multi-repo pipelines: auto-readies (or HITL-holds) a dependent slice's draft PR once its cross-repo upstream PR merges; rides the stacked-PR reconciler cadence (#3393 slice-5) -├── slice_green_gate.py # Per-slice green gate: sandboxed one-shot Job runs the repo's configured checks at the integration-branch tip; staged rollout via EGG_SLICE_GREEN_GATE (off/log/on, default on — on withholds the slice PR on a red verdict, log runs the checks and logs the verdict without blocking), fail-open on infra errors (#3398) and on infra-signature-tagged reds inside check execution (#3417) +├── slice_green_gate.py # Per-slice green gate: sandboxed one-shot Job runs the repo's configured checks at the integration-branch tip; staged rollout via EGG_SLICE_GREEN_GATE (off/log/on, default on — on withholds the slice PR on a red verdict, log runs the checks and logs the verdict without blocking), fail-open on infra errors (#3398) and on infra-signature-tagged reds inside check execution (#3417); Stage A (#3409) auto-remediates via each check's optional repositories.yaml `fix:` command and commits + pushes the fix to the integration branch instead of blocking, but only in `on` mode and only when every *genuine* red (infra-tagged reds are excluded first under the default EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=on, #3417) re-ran green, a final full re-run of every check the gate runs (EGG_SLICE_GREEN_GATE_SKIP_CHECKS excludes names, default `security`) against the fixed tree was green, the runner's tree gained no new non-ignored untracked files (from the fix or from the checks themselves — the baseline predates the first check), and at least one tracked file in it is modified so there is something to commit; when *every* red is infra-tagged the gate fails open and returns before the autofix decision, discarding a fix that re-ran green rather than committing it ├── action_guards.py # Formal BRC state machine action guards (preconditions for propose/ack/nack/confirm/withdraw) ├── approval_matrix.py # Per-reviewer ACK/NACK matrix for BRC consensus ├── attestation_schemas.py # Attestation payload validation for BRC proposals diff --git a/docs/guides/sdlc-pipeline.md b/docs/guides/sdlc-pipeline.md index 329e2b5a7..ea12fe0a3 100644 --- a/docs/guides/sdlc-pipeline.md +++ b/docs/guides/sdlc-pipeline.md @@ -992,10 +992,17 @@ repo_settings: checks: - name: lint command: npm run lint + fix: npm run lint -- --fix # optional: green-gate auto-remediation (#3409) - name: test command: npm test ``` +A check may optionally set `fix` to a single shell command string that auto-remediates it. It must be a *string*: a YAML list is silently coerced to its Python repr (`"['make fmt', 'make lint-fix']"`) and will never run, so chain steps with `&&` or point at a make target; an empty or omitted value (a bare `fix:`, `fix: ""`, `fix: false`, `fix: 0`) is dropped entirely, leaving the check with no fix and a block message that never mentions `fix`. When the per-slice green gate finds that check red at the slice tip, it runs the fix and re-runs the check. It commits and pushes the result to the slice integration branch only when **all** of the following hold (#3409): every *genuine* red check's fix turned it green (reds tagged with an infra signature are excluded before this decision under the default `EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=on`, #3417 — set that switch to `off` and every red, infra-tagged or not, must carry a fix that re-ran green); a final full re-run of every check the gate runs against the all-fixes-applied tree is also green; the runner's tree gained no new non-ignored untracked files, whether from the fix *or* from the checks themselves — the baseline is captured before the first check runs (the gate stages with `git add -u`, which would silently drop them; gitignored droppings don't count); at least one tracked file in that tree is modified, so there is something to commit; the runner reported a final-verification verdict at all and could determine that untracked count (a best-effort git failure in the runner is treated as unsafe); and the gate is running in `on` mode. Every refusal is logged as `autofix_block_reason` on the orchestrator's `Green gate red` line — a `log`-mode refusal is not one of them (that line reads "autofix available but not applied" and carries no such field) — and the reason reaches the slice failure message only when every genuine red's own re-run went green; otherwise it stays in the orchestrator's structured log. When *every* red is infra-tagged the gate fails open and returns **before** this decision, so even a fix that re-ran green is discarded with the worktree rather than committed. Only configure deterministic auto-remediations here; checks without `fix`, or whose re-run stays red, route red verdicts back to the slice team unchanged. + +**`on` is the default mode — no opt-in is required.** `EGG_SLICE_GREEN_GATE` is unset in the shipped configuration, and both an unset and an unrecognised value resolve to `on`, so autofix commits and pushes to your slice integration branch unless you turn it down. Set `EGG_SLICE_GREEN_GATE=log` to keep the fix running as a soak signal while committing and pushing nothing, or `off` to stop the gate before it spawns the runner at all — under `off` the fix never executes. See [slice-dag.md](../architecture/slice-dag.md) for the rollout switch. + +`fix` belongs to the per-slice green gate (slice-DAG implement pipelines) and is unrelated to the `check-fixer` agent ([Built-in Checks](#built-in-checks)) or the `fixer` DAG step ([Check DAG Configuration](#check-dag-configuration)). The tester step ignores `fix`, so setting it on a non-slice pipeline has no effect. `fix` is also a silent no-op on a check the gate skips: `EGG_SLICE_GREEN_GATE_SKIP_CHECKS` excludes checks by name before the gate runs, and defaults to `security`. + When configured, the tester runs these commands sequentially instead of auto-discovering test/lint commands. This is useful when: - Auto-discovery doesn't find the right commands - You want to run checks in a specific order