From fa75f787eb7cdfe9c70c536cc13c26cb5d7778f6 Mon Sep 17 00:00:00 2001 From: James Wiesebron Date: Mon, 6 Jul 2026 10:48:10 -0700 Subject: [PATCH 1/4] Green gate: fail open on infra-signature reds inside check execution Closes #3417. The runner now tags each red check whose full combined output matches an exact infra signature (the sandbox git wrapper's gateway-down / missing-env / session-auth errors, the kernel's ENOSPC message) or whose process died by SIGKILL. A verdict where every red check is infra-tagged fails open with a loud warning; mixed verdicts block on the genuine reds only. Enabled by default; EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=off restores strict every-red-blocks behavior. --- docs/architecture/slice-dag.md | 4 +- docs/development/STRUCTURE.md | 2 +- orchestrator/slice_green_gate.py | 142 +++++++++++-- orchestrator/tests/test_slice_green_gate.py | 218 +++++++++++++++++++- 4 files changed, 342 insertions(+), 24 deletions(-) diff --git a/docs/architecture/slice-dag.md b/docs/architecture/slice-dag.md index db4bdcc448..f95af75d21 100644 --- a/docs/architecture/slice-dag.md +++ b/docs/architecture/slice-dag.md @@ -480,7 +480,8 @@ shape: spawns a sandboxed one-shot check-runner Job to execute the repo's configured checks at the integration-branch tip and blocks PR-open on a red verdict; staged rollout via - `EGG_SLICE_GREEN_GATE`, fail-open on infra errors) — calls + `EGG_SLICE_GREEN_GATE`, fail-open on infra errors, including + infra-signature-tagged reds inside check execution, #3417) — calls `GatewayClient.create_slice_pr` with `base` resolved from the slice's DAG parent (root → pipeline branch; child → parent's integration branch). On failure the worker calls @@ -985,6 +986,7 @@ on parse failure. The green-gate knobs below are read directly via | `EGG_SLICE_GREEN_GATE` | str | `off` | Per-slice green gate rollout switch (#3398): `off` skips the gate entirely; `log` runs the repo's configured checks at the slice tip and logs a red verdict without blocking; `on` blocks slice PR-open on a red verdict. Case-insensitive, with aliases — `on` also accepts `1`/`true`/`yes`, and `log` also accepts `log-only`/`log_only`. Unknown values resolve to `off`. | | `EGG_SLICE_GREEN_GATE_SKIP_CHECKS` | str (comma-separated) | `security` | Configured check *names* (from `repositories.yaml` `checks`) the gate skips. | | `EGG_SLICE_GREEN_GATE_TIMEOUT_SECONDS` | int | 1800 | Wall-clock budget for the check-runner pod (spawn-to-terminal); a hung suite degrades to fail-open rather than wedging the slice close. | +| `EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN` | str | `on` | Infra-red fail-open (#3417): the runner tags red checks whose full output matches an exact infra signature (the sandbox git wrapper's gateway-down / missing-env / session-auth errors, the kernel's ENOSPC message) or whose process died by SIGKILL; a verdict where *every* red check is infra-tagged fails open instead of blocking, and mixed verdicts block on the genuine reds only. `off`/`0`/`false`/`no` restores strict every-red-blocks behavior; any other value resolves to `on`. | ### Per-pipeline vs. global slice caps diff --git a/docs/development/STRUCTURE.md b/docs/development/STRUCTURE.md index be2672c0ba..9dd83cd757 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 and blocks PR-open on red; staged rollout via EGG_SLICE_GREEN_GATE (off/log/on), fail-open on infra errors (#3398) +├── slice_green_gate.py # Per-slice green gate: sandboxed one-shot Job runs the repo's configured checks at the integration-branch tip and blocks PR-open on red; staged rollout via EGG_SLICE_GREEN_GATE (off/log/on), fail-open on infra errors (#3398) and on infra-signature-tagged reds inside check execution (#3417) ├── 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/orchestrator/slice_green_gate.py b/orchestrator/slice_green_gate.py index 5bf41e1bd2..0bdc43a725 100644 --- a/orchestrator/slice_green_gate.py +++ b/orchestrator/slice_green_gate.py @@ -38,17 +38,25 @@ failed). The caller records the slice failure, which routes through the existing cascade + ``OVERSEER_ALERT`` machinery. - The fail-open guarantee only covers infrastructure failures the - *orchestrator* observes before/around check execution. Once the runner - is executing the checks, an infrastructure fault *inside* that - execution — a transient gateway hiccup on a git call, a mid-run - session-token expiry, an OOM-killed test worker, disk pressure — - exits the check non-zero and surfaces as ``ok:false``, i.e. a - definitive red that ``on`` mode blocks on. This is inherent to - shelling out to checks (CI has the same property); the staged - ``off → log → on`` rollout de-risks it, but ``on`` mode does not - distinguish an infra-induced red inside a check from a genuine - check failure. + Fail-open also covers a narrow class of infrastructure faults *inside* + check execution (#3417): the runner tags each red check whose combined + output contains one of the exact, high-confidence infra signatures in + ``_INFRA_OUTPUT_SIGNATURES`` (the sandbox git wrapper's gateway-down / + missing-env / session-auth errors, the kernel's ENOSPC message) or + whose process died by SIGKILL (the OOM killer). When *every* red check + in a verdict is infra-tagged, the gate fails open with a loud warning + instead of blocking; when genuine reds and infra-tagged reds mix, the + gate blocks on the genuine reds only. Signature matching runs over the + check's **full** output, not the truncated verdict tail, so an early + gateway error can't scroll out of detection. + + This classification is security-relevant: the signatures are matched + against untrusted check output, so a check that *prints* a signature + while genuinely failing fails itself open. That is why the allowlist + is a handful of exact strings emitted only by egg's own plumbing, + never fuzzy patterns, and why + ``EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=off`` restores the strict + every-red-blocks behavior. Rollout is staged via ``EGG_SLICE_GREEN_GATE``: ``off`` (default) → ``log`` (run checks, log the verdict loudly, never block — the soak mode @@ -104,6 +112,38 @@ GREEN_GATE_SKIP_CHECKS_ENV_VAR = "EGG_SLICE_GREEN_GATE_SKIP_CHECKS" _DEFAULT_SKIP_CHECKS = "security" +# Operator switch for the #3417 infra-red fail-open. Default "on": a +# red verdict where every failed check matches an infra signature fails +# open instead of blocking. "off" (or 0/false/no) restores the strict +# pre-#3417 behavior where every red blocks. Any other value degrades +# to the default, matching green_gate_mode's typo posture. +GREEN_GATE_INFRA_FAIL_OPEN_ENV_VAR = "EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN" +_INFRA_FAIL_OPEN_DISABLED_VALUES = frozenset({"off", "0", "false", "no"}) + +# Exact output signatures that identify an infrastructure fault inside a +# check rather than a genuine failure (#3417). Security-relevant: these +# are substring-matched against untrusted check output, so a check that +# prints one fails itself open. Keep the list to exact strings emitted +# only by egg's own plumbing (sandbox/scripts/git) or the kernel; never +# add fuzzy patterns like "Killed" or "connection refused" that real +# test output can legitimately contain. +_INFRA_OUTPUT_SIGNATURES = ( + # sandbox/scripts/git: GATEWAY_URL was not wired into the runner pod. + "ERROR: GATEWAY_URL environment variable is not set.", + # sandbox/scripts/git show_gateway_unavailable(): the wrapper's + # gateway health probe failed (gateway restart / network blip). + "GATEWAY SIDECAR NOT AVAILABLE", + # sandbox/scripts/git: session token missing from the environment. + "ERROR: EGG_SESSION_TOKEN not set. Session required for gateway access", + # sandbox/scripts/git: gateway returned HTTP 401, i.e. a mid-run + # session-token expiry or revocation. + "Authentication failed - check session token", + # Kernel ENOSPC strerror: disk pressure on the node. A check can + # only hit this when the node is genuinely out of space, which is + # infrastructure either way. + "No space left on device", +) + # Wall-clock budget for the runner pod (spawn-to-terminal). A slice's # changeset-narrowed ``make test`` normally finishes well inside this; # the ceiling exists so a hung suite degrades to fail-open instead of @@ -148,12 +188,33 @@ # when the repo config requires one is exactly such an infra failure — # proceeding would red every check with "command not found" and block # the slice for a toolchain-packaging problem that is not its fault. +# +# Infra classification (#3417) happens here, runner-side, because only +# the runner sees a check's *full* output: the verdict carries a +# truncated tail, and an early gateway error (e.g. the test selector's +# first git call failing) could scroll out of it. Each red check gets +# an ``infra`` field: the matched signature string, a SIGKILL note, or +# None for a genuine failure. The orchestrator decides what to do with +# the tags; the runner only reports. _RUNNER_PROGRAM = """ import json, os, shutil, subprocess, sys, time checks = json.loads(os.environ["EGG_GREEN_GATE_CHECKS"]) repo_dir = os.environ["EGG_GREEN_GATE_REPO_DIR"] tail = int(os.environ.get("EGG_GREEN_GATE_OUTPUT_TAIL", "4000")) +infra_signatures = json.loads(os.environ.get("EGG_GREEN_GATE_INFRA_SIGNATURES", "[]")) + + +def classify_infra(rc, out): + # A SIGKILLed check (rc -9 when bash itself dies, 137 when bash + # reports a killed child) is the OOM killer or the pod deadline, + # never a check verdict: no test runner signals failure via SIGKILL. + if rc in (-9, 137): + return "check process died by SIGKILL (exit %s): OOM kill or pod deadline" % rc + for sig in infra_signatures: + if sig in out: + return sig + return None def restore_prebuilt(target_dir): @@ -224,6 +285,7 @@ def copy_if_missing(src, dst, **kwargs): "exit_code": rc, "duration_seconds": round(time.monotonic() - started, 1), "output_tail": out[-tail:], + "infra": classify_infra(rc, out) if rc != 0 else None, } ) @@ -245,6 +307,17 @@ def green_gate_mode() -> Literal["off", "log", "on"]: return "off" +def _infra_fail_open_enabled() -> bool: + """Resolve the #3417 infra-red fail-open switch (default on). + + Only the exact disabled values turn it off; anything else degrades + to the default. Mirrors ``green_gate_mode``'s posture: an operator + typo resolves to the documented default behavior. + """ + raw = os.environ.get(GREEN_GATE_INFRA_FAIL_OPEN_ENV_VAR, "on").strip().lower() + return raw not in _INFRA_FAIL_OPEN_DISABLED_VALUES + + def _gate_checks(repo: str) -> list[dict[str, str]]: """Return the configured checks the gate runs for ``repo``. @@ -341,6 +414,7 @@ def _build_runner_job_manifest( full_env["EGG_GREEN_GATE_CHECKS"] = json.dumps(checks) full_env["EGG_GREEN_GATE_REPO_DIR"] = repo_dir full_env["EGG_GREEN_GATE_OUTPUT_TAIL"] = str(_VERDICT_OUTPUT_TAIL_CHARS) + full_env["EGG_GREEN_GATE_INFRA_SIGNATURES"] = json.dumps(list(_INFRA_OUTPUT_SIGNATURES)) volumes = [] volume_mounts = [] @@ -591,12 +665,13 @@ def run_slice_green_gate( """Execute the repo's configured checks at the slice tip; gate PR-open (#3398). Runs after slice consensus and the #3125 evidence gate, before any - close side effect. Returns ``None`` when the slice may close (checks - green, gate off/log-mode, or an infrastructure failure — fail-open), - or a human-readable failure string naming the red checks — the - caller records the slice failure with it, routing through the - existing cascade + OVERSEER_ALERT machinery instead of opening a - red PR. + close side effect. Returns ``None`` when the slice may close: checks + green, gate off/log-mode, an infrastructure failure (fail-open), or + a red verdict where every failed check carries an infra tag (#3417). + Otherwise returns a human-readable failure string naming the + genuinely red checks; the caller records the slice failure with it, + routing through the existing cascade + OVERSEER_ALERT machinery + instead of opening a red PR. The runner gets its own gateway worktree forked from ``origin/`` (both ``base_branch`` and @@ -789,7 +864,36 @@ def run_slice_green_gate( ) return None - failed_names = ", ".join(str(c.get("name")) for c in failed) + # Infra-red fail-open (#3417): a red check the runner tagged with + # an infra signature is an infrastructure fault inside check + # execution, not a verdict on the slice. Fail open when every red + # is infra-tagged; when genuine and infra reds mix, block on the + # genuine reds only so the failure routed to the cascade doesn't + # send anyone chasing an infra ghost. + genuine_failed = failed + if _infra_fail_open_enabled(): + infra_failed = [c for c in failed if c.get("infra")] + genuine_failed = [c for c in failed if not c.get("infra")] + if infra_failed: + logger.warning( + "Green gate: red checks match infrastructure signatures (#3417)", + pipeline_id=pipeline_id, + slice_id=slice_id, + gate_id=gate_id, + infra_checks={str(c.get("name")): str(c.get("infra")) for c in infra_failed}, + ) + if not genuine_failed: + logger.warning( + "Green gate skipped: every red check is infra-induced, failing open (#3417)", + pipeline_id=pipeline_id, + slice_id=slice_id, + gate_id=gate_id, + integration_branch=integration_branch, + mode=mode, + ) + return None + + failed_names = ", ".join(str(c.get("name")) for c in genuine_failed) logger.error( "Green gate red: configured checks failed at the slice tip (#3398)", pipeline_id=pipeline_id, @@ -804,7 +908,7 @@ def run_slice_green_gate( return ( f"slice {slice_id}: green gate failed — configured checks are red " f"at integration branch {integration_branch} tip: {failed_names}.\n\n" - f"{_format_failed_checks(failed)}\n\n" + f"{_format_failed_checks(genuine_failed)}\n\n" f"Fix the failures on {integration_branch} and restart the slice; " f"set {GREEN_GATE_ENV_VAR}=off to bypass." ) diff --git a/orchestrator/tests/test_slice_green_gate.py b/orchestrator/tests/test_slice_green_gate.py index 42dd0983cf..b0f33f0bd3 100644 --- a/orchestrator/tests/test_slice_green_gate.py +++ b/orchestrator/tests/test_slice_green_gate.py @@ -10,15 +10,20 @@ * ``parse_verdict`` — sentinel-line extraction from noisy pod logs. * ``_RUNNER_PROGRAM`` — executed for real in a subprocess: check execution + verdict shape, output tails, the prebuilt-deps restore - (copy-if-missing), and the required-but-missing infra exit. + (copy-if-missing), the required-but-missing infra exit, and the + #3417 infra tagging (signature match over full output, SIGKILL exit, + green checks never tagged). * ``_build_runner_job_manifest`` — labels (NetworkPolicy component label present; monitor/agent-supervision labels absent), env, mounts, deadline. * ``run_slice_green_gate`` — gate wiring: kill switch, fail-open on every infrastructure failure (worktree, session, submit, timeout, unparseable verdict), fail-closed only on a definitive red verdict, - log-mode never blocking, and cleanup (job/session/worktree) on every - path that created the resource. + log-mode never blocking, cleanup (job/session/worktree) on every + path that created the resource, and the #3417 infra-red fail-open + (all-infra reds fail open, mixed reds block on the genuine ones, + ``EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=off`` restores strict + blocking). """ from __future__ import annotations @@ -64,6 +69,7 @@ def gate_env(monkeypatch: pytest.MonkeyPatch) -> pytest.MonkeyPatch: sgg.GREEN_GATE_ENV_VAR, sgg.GREEN_GATE_SKIP_CHECKS_ENV_VAR, sgg.GREEN_GATE_TIMEOUT_ENV_VAR, + sgg.GREEN_GATE_INFRA_FAIL_OPEN_ENV_VAR, ): monkeypatch.delenv(var, raising=False) return monkeypatch @@ -150,6 +156,21 @@ def test_config_error_fails_open(self) -> None: assert sgg._repo_requires_prebuilt(REPO) is False +class TestInfraFailOpenEnabled: + def test_default_is_on(self, gate_env: pytest.MonkeyPatch) -> None: + assert sgg._infra_fail_open_enabled() is True + + @pytest.mark.parametrize("value", ["off", "OFF", " 0 ", "false", "no"]) + def test_disabled_values(self, gate_env: pytest.MonkeyPatch, value: str) -> None: + gate_env.setenv(sgg.GREEN_GATE_INFRA_FAIL_OPEN_ENV_VAR, value) + assert sgg._infra_fail_open_enabled() is False + + @pytest.mark.parametrize("value", ["on", "1", "true", "", "banana"]) + def test_everything_else_is_on(self, gate_env: pytest.MonkeyPatch, value: str) -> None: + gate_env.setenv(sgg.GREEN_GATE_INFRA_FAIL_OPEN_ENV_VAR, value) + assert sgg._infra_fail_open_enabled() is True + + class TestGateTimeout: def test_default(self, gate_env: pytest.MonkeyPatch) -> None: assert sgg._gate_timeout_seconds() == sgg._DEFAULT_TIMEOUT_SECONDS @@ -223,6 +244,7 @@ def _run_runner( "EGG_GREEN_GATE_CHECKS": json.dumps(checks), "EGG_GREEN_GATE_REPO_DIR": str(repo_dir), "EGG_GREEN_GATE_OUTPUT_TAIL": "200", + "EGG_GREEN_GATE_INFRA_SIGNATURES": json.dumps(list(sgg._INFRA_OUTPUT_SIGNATURES)), "EGG_GREEN_GATE_REQUIRE_PREBUILT": require_prebuilt, "EGG_GREEN_GATE_PREBUILT_BASE": str( prebuilt_base if prebuilt_base is not None else tmp_path / "no-prebuilt" @@ -275,6 +297,79 @@ def test_checks_run_in_repo_dir(self, tmp_path: Path) -> None: assert verdict is not None assert "present" in verdict["checks"][0]["output_tail"] + def test_red_check_with_infra_signature_is_tagged(self, tmp_path: Path) -> None: + proc = _run_runner( + tmp_path, + [ + { + "name": "infra-red", + "command": "echo 'GATEWAY SIDECAR NOT AVAILABLE'; exit 1", + } + ], + ) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + check = verdict["checks"][0] + assert check["ok"] is False + assert check["infra"] == "GATEWAY SIDECAR NOT AVAILABLE" + + def test_genuine_red_check_is_not_tagged(self, tmp_path: Path) -> None: + proc = _run_runner( + tmp_path, + [{"name": "genuine-red", "command": "echo 'FAILED test_x'; exit 2"}], + ) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + assert verdict["checks"][0]["infra"] is None + + def test_green_check_never_tagged_even_with_signature_output(self, tmp_path: Path) -> None: + proc = _run_runner( + tmp_path, + [ + { + "name": "green-with-noise", + "command": "echo 'GATEWAY SIDECAR NOT AVAILABLE'; exit 0", + } + ], + ) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + check = verdict["checks"][0] + assert check["ok"] is True + assert check["infra"] is None + + def test_sigkilled_check_is_tagged(self, tmp_path: Path) -> None: + # bash kills itself with SIGKILL, so subprocess reports rc -9: + # the same shape as an OOM kill of the check shell. + proc = _run_runner(tmp_path, [{"name": "oom", "command": "kill -9 $$"}]) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + check = verdict["checks"][0] + assert check["ok"] is False + assert check["infra"] is not None + assert "SIGKILL" in check["infra"] + + def test_signature_scrolled_out_of_tail_is_still_detected(self, tmp_path: Path) -> None: + # The infra error appears early, then enough output follows to + # push it past the 200-char tail. Detection must run over the + # full output, not the truncated tail (#3417). + proc = _run_runner( + tmp_path, + [ + { + "name": "early-infra", + "command": ( + "echo 'GATEWAY SIDECAR NOT AVAILABLE'; yes filler-line | head -50; exit 1" + ), + } + ], + ) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + check = verdict["checks"][0] + assert "GATEWAY SIDECAR NOT AVAILABLE" not in check["output_tail"] + assert check["infra"] == "GATEWAY SIDECAR NOT AVAILABLE" + def test_prebuilt_restore_copy_if_missing(self, tmp_path: Path) -> None: prebuilt = tmp_path / "prebuilt" / "jwbron--egg" (prebuilt / ".venv" / "bin").mkdir(parents=True) @@ -370,6 +465,16 @@ def test_no_agent_supervision_labels(self) -> None: for forbidden in ("egg.orchestrator", "egg.agent.role", "egg.slice.id"): assert forbidden not in all_labels + def test_env_carries_infra_signatures(self) -> None: + manifest = _manifest() + env = { + e["name"]: e["value"] + for e in manifest["spec"]["template"]["spec"]["containers"][0]["env"] + } + assert json.loads(env["EGG_GREEN_GATE_INFRA_SIGNATURES"]) == list( + sgg._INFRA_OUTPUT_SIGNATURES + ) + def test_env_carries_checks_and_repo_dir(self) -> None: manifest = _manifest() env = { @@ -635,6 +740,113 @@ def test_red_verdict_in_log_mode_does_not_block( # Log mode still runs the checks — it only skips the block. submit.assert_called_once() + def test_all_infra_reds_fail_open( + self, enabled_gate: pytest.MonkeyPatch, configured_checks: None + ) -> None: + spawner = _spawner() + log = _verdict_line( + [ + {"name": "lint", "ok": True, "exit_code": 0, "output_tail": "", "infra": None}, + { + "name": "test", + "ok": False, + "exit_code": 1, + "output_tail": "GATEWAY SIDECAR NOT AVAILABLE", + "infra": "GATEWAY SIDECAR NOT AVAILABLE", + }, + ] + ) + with ( + patch.object(sgg, "_submit_runner_job"), + patch.object(sgg, "_wait_for_runner_pod", return_value=_terminal_pod()), + patch.object(sgg, "_read_runner_log", return_value=log), + patch.object(sgg, "_delete_runner_job") as delete_job, + ): + assert _run_gate(spawner) is None + # Fail-open still cleans up everything it created. + delete_job.assert_called_once() + spawner.gateway.delete_session_by_container.assert_called_once() + spawner.gateway.delete_worktrees.assert_called_once() + + def test_mixed_reds_block_on_genuine_only( + self, enabled_gate: pytest.MonkeyPatch, configured_checks: None + ) -> None: + spawner = _spawner() + log = _verdict_line( + [ + { + "name": "lint", + "ok": False, + "exit_code": 1, + "output_tail": "GATEWAY SIDECAR NOT AVAILABLE", + "infra": "GATEWAY SIDECAR NOT AVAILABLE", + }, + { + "name": "test", + "ok": False, + "exit_code": 2, + "output_tail": "FAILED tests/test_x.py::test_y", + "infra": None, + }, + ] + ) + with ( + patch.object(sgg, "_submit_runner_job"), + patch.object(sgg, "_wait_for_runner_pod", return_value=_terminal_pod()), + patch.object(sgg, "_read_runner_log", return_value=log), + patch.object(sgg, "_delete_runner_job"), + ): + failure = _run_gate(spawner) + assert failure is not None + assert "test" in failure + assert "FAILED tests/test_x.py::test_y" in failure + # The infra-tagged red must not be presented as a slice failure: + # its name and output stay out of the cascade-routed message. + assert "GATEWAY SIDECAR NOT AVAILABLE" not in failure + assert "lint" not in failure + + def test_infra_fail_open_switch_off_blocks_on_infra_reds( + self, enabled_gate: pytest.MonkeyPatch, configured_checks: None + ) -> None: + enabled_gate.setenv(sgg.GREEN_GATE_INFRA_FAIL_OPEN_ENV_VAR, "off") + spawner = _spawner() + log = _verdict_line( + [ + { + "name": "test", + "ok": False, + "exit_code": 1, + "output_tail": "GATEWAY SIDECAR NOT AVAILABLE", + "infra": "GATEWAY SIDECAR NOT AVAILABLE", + } + ] + ) + with ( + patch.object(sgg, "_submit_runner_job"), + patch.object(sgg, "_wait_for_runner_pod", return_value=_terminal_pod()), + patch.object(sgg, "_read_runner_log", return_value=log), + patch.object(sgg, "_delete_runner_job"), + ): + failure = _run_gate(spawner) + assert failure is not None + assert "test" in failure + + def test_pre_3417_verdict_without_infra_field_blocks( + self, enabled_gate: pytest.MonkeyPatch, configured_checks: None + ) -> None: + # A verdict whose checks carry no ``infra`` key (an in-flight + # runner from before the #3417 rollout) is treated as genuinely + # red: absence of the tag must never fail open. + spawner = _spawner() + log = _verdict_line([{"name": "test", "ok": False, "exit_code": 2, "output_tail": "x"}]) + with ( + patch.object(sgg, "_submit_runner_job"), + patch.object(sgg, "_wait_for_runner_pod", return_value=_terminal_pod()), + patch.object(sgg, "_read_runner_log", return_value=log), + patch.object(sgg, "_delete_runner_job"), + ): + assert _run_gate(spawner) is not None + def test_worktree_forked_from_integration_branch( self, enabled_gate: pytest.MonkeyPatch, configured_checks: None ) -> None: From 76dd4548058a4b899c239397110bcac02c71f168 Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:02:38 +0000 Subject: [PATCH 2/4] Green gate: whole-line match git-wrapper infra signatures to close self-masking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review flagged that this PR puts the exact infra-signature literals into egg's own test_slice_green_gate.py, making the fail-open reachable in egg's normal path: a genuine regression in a green-gate test prints a signature via pytest assertion introspection, gets tagged infra, and the gate masks its own red (self-masking). Split _INFRA_OUTPUT_SIGNATURES into _INFRA_LINE_SIGNATURES (the four git-wrapper strings, now matched whole-line — the stripped output line must equal the signature) and _INFRA_SUBSTRING_SIGNATURES (the kernel ENOSPC strerror, still substring-matched since it surfaces mid-message). pytest introspection always embeds the literal mid-line behind an E/assert/diff-marker/quote prefix, so whole-line matching rejects it while still tagging the git wrapper's bare-echo lines. Also address the two non-blocking notes: document that a SIGKILL/OOM from the slice's own code is indistinguishable and accepted as fail-open, and correct the classify_infra comment — a pod-deadline kill takes down the runner (PID 1), not a per-check subprocess, so it fails open via the missing-verdict path rather than surfacing as a per-check 137. Adds 3 tests (mid-line literal not tagged, indented banner line still tagged, ENOSPC substring mid-line still tagged); 86 pass. --- orchestrator/slice_green_gate.py | 120 +++++++++++++++----- orchestrator/tests/test_slice_green_gate.py | 71 +++++++++++- 2 files changed, 156 insertions(+), 35 deletions(-) diff --git a/orchestrator/slice_green_gate.py b/orchestrator/slice_green_gate.py index 0bdc43a725..09869431b5 100644 --- a/orchestrator/slice_green_gate.py +++ b/orchestrator/slice_green_gate.py @@ -40,23 +40,32 @@ Fail-open also covers a narrow class of infrastructure faults *inside* check execution (#3417): the runner tags each red check whose combined - output contains one of the exact, high-confidence infra signatures in - ``_INFRA_OUTPUT_SIGNATURES`` (the sandbox git wrapper's gateway-down / - missing-env / session-auth errors, the kernel's ENOSPC message) or - whose process died by SIGKILL (the OOM killer). When *every* red check - in a verdict is infra-tagged, the gate fails open with a loud warning - instead of blocking; when genuine reds and infra-tagged reds mix, the - gate blocks on the genuine reds only. Signature matching runs over the - check's **full** output, not the truncated verdict tail, so an early - gateway error can't scroll out of detection. + output contains one of the exact, high-confidence infra signatures (the + sandbox git wrapper's gateway-down / missing-env / session-auth errors, + the kernel's ENOSPC message) or whose process died by SIGKILL (the OOM + killer). When *every* red check in a verdict is infra-tagged, the gate + fails open with a loud warning instead of blocking; when genuine reds + and infra-tagged reds mix, the gate blocks on the genuine reds only. + Signature matching runs over the check's **full** output, not the + truncated verdict tail, so an early gateway error can't scroll out of + detection. This classification is security-relevant: the signatures are matched against untrusted check output, so a check that *prints* a signature - while genuinely failing fails itself open. That is why the allowlist - is a handful of exact strings emitted only by egg's own plumbing, - never fuzzy patterns, and why - ``EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=off`` restores the strict - every-red-blocks behavior. + while genuinely failing could fail itself open. Two guards keep that + surface tight. First, the allowlist is a handful of exact strings + emitted only by egg's own plumbing, never fuzzy patterns. Second, the + git-wrapper signatures are matched **whole-line** (the stripped output + line must *equal* the signature), not as a substring: this PR puts + those literals into egg's own ``test_slice_green_gate.py``, so a + genuine regression there would print one via pytest assertion + introspection — always mid-line behind an ``E``/``assert``/diff-marker + prefix — and whole-line matching rejects that, so the gate can't mask + its own red regression (see ``_INFRA_LINE_SIGNATURES``). The one + residual, accepted, hole is the SIGKILL arm: an OOM caused by the + slice's *own* memory-explosion bug is indistinguishable from an infra + OOM and fails open. ``EGG_SLICE_GREEN_GATE_INFRA_FAIL_OPEN=off`` + restores the strict every-red-blocks behavior. Rollout is staged via ``EGG_SLICE_GREEN_GATE``: ``off`` (default) → ``log`` (run checks, log the verdict loudly, never block — the soak mode @@ -121,29 +130,51 @@ _INFRA_FAIL_OPEN_DISABLED_VALUES = frozenset({"off", "0", "false", "no"}) # Exact output signatures that identify an infrastructure fault inside a -# check rather than a genuine failure (#3417). Security-relevant: these -# are substring-matched against untrusted check output, so a check that -# prints one fails itself open. Keep the list to exact strings emitted -# only by egg's own plumbing (sandbox/scripts/git) or the kernel; never -# add fuzzy patterns like "Killed" or "connection refused" that real -# test output can legitimately contain. -_INFRA_OUTPUT_SIGNATURES = ( +# check rather than a genuine failure (#3417). Security-relevant: matched +# against untrusted check output, so a check that prints one fails itself +# open. Keep the list to exact strings emitted only by egg's own plumbing +# (sandbox/scripts/git) or the kernel; never add fuzzy patterns like +# "Killed" or "connection refused" that real test output can legitimately +# contain. The two groups differ only in match *mode*: +# +# ``_INFRA_LINE_SIGNATURES`` are matched **whole-line** (a stripped +# output line must equal the signature), not as a substring. The sandbox +# git wrapper emits each as a bare ``echo`` line, so whole-line matching +# still catches the real fault — but it closes a self-masking hole the +# #3417 review flagged: this PR puts these exact literals into egg's own +# ``test_slice_green_gate.py``, so a *genuine* regression in a green-gate +# test would print one via pytest assertion introspection (``assert None +# == 'GATEWAY SIDECAR NOT AVAILABLE'``, a source-repr fixture literal, a +# unified-diff ``-`` line). Every such form embeds the signature mid-line +# behind an ``E ``/``assert``/``- ``/quote prefix, so whole-line matching +# rejects it — the gate can no longer tag its own red regression as infra +# and fail open (which would hide the very failure the gate exists to +# catch, including a break in this tagging logic itself). +_INFRA_LINE_SIGNATURES = ( # sandbox/scripts/git: GATEWAY_URL was not wired into the runner pod. "ERROR: GATEWAY_URL environment variable is not set.", # sandbox/scripts/git show_gateway_unavailable(): the wrapper's # gateway health probe failed (gateway restart / network blip). + # Emitted inside a banner with leading whitespace — whole-line + # matching strips it before comparing. "GATEWAY SIDECAR NOT AVAILABLE", # sandbox/scripts/git: session token missing from the environment. "ERROR: EGG_SESSION_TOKEN not set. Session required for gateway access", # sandbox/scripts/git: gateway returned HTTP 401, i.e. a mid-run # session-token expiry or revocation. "Authentication failed - check session token", - # Kernel ENOSPC strerror: disk pressure on the node. A check can - # only hit this when the node is genuinely out of space, which is - # infrastructure either way. - "No space left on device", ) +# ``_INFRA_SUBSTRING_SIGNATURES`` are matched as a substring: the kernel +# ENOSPC strerror surfaces embedded in a larger message (``[Errno 28] No +# space left on device``) rather than on its own line, so whole-line +# matching would miss it. Disk pressure is infrastructure however it +# surfaces, and this string is far less likely than the git-wrapper lines +# to appear as a bare test literal (the #3417 review's own assessment — +# it called the four git-wrapper signatures the fragile ones and this arm +# robust), so keeping it substring-matched is a deliberate, narrow risk. +_INFRA_SUBSTRING_SIGNATURES = ("No space left on device",) + # Wall-clock budget for the runner pod (spawn-to-terminal). A slice's # changeset-narrowed ``make test`` normally finishes well inside this; # the ceiling exists so a hung suite degrades to fail-open instead of @@ -202,16 +233,38 @@ checks = json.loads(os.environ["EGG_GREEN_GATE_CHECKS"]) repo_dir = os.environ["EGG_GREEN_GATE_REPO_DIR"] tail = int(os.environ.get("EGG_GREEN_GATE_OUTPUT_TAIL", "4000")) -infra_signatures = json.loads(os.environ.get("EGG_GREEN_GATE_INFRA_SIGNATURES", "[]")) +infra_signatures = json.loads(os.environ.get("EGG_GREEN_GATE_INFRA_SIGNATURES", "{}")) +infra_line_signatures = infra_signatures.get("line", []) +infra_substring_signatures = infra_signatures.get("substring", []) def classify_infra(rc, out): # A SIGKILLed check (rc -9 when bash itself dies, 137 when bash - # reports a killed child) is the OOM killer or the pod deadline, - # never a check verdict: no test runner signals failure via SIGKILL. + # reports a killed child) is the OOM killer: no test runner signals + # failure via SIGKILL. Caveat (#3417 review): an OOM caused by the + # *slice's own* code — a memory-explosion bug in the code under test — + # is indistinguishable here from an infra OOM and is accepted as + # fail-open; the SIGKILL arm is the broad one. A pod-deadline kill + # takes down the runner (PID 1 python), not a check subprocess, so it + # never surfaces as a per-check 137 — that case fails open via the + # orchestrator's missing-verdict path, not here. if rc in (-9, 137): - return "check process died by SIGKILL (exit %s): OOM kill or pod deadline" % rc - for sig in infra_signatures: + return "check process died by SIGKILL (exit %s): OOM killer" % rc + # Whole-line match for the git-wrapper signatures: they are emitted as + # standalone lines, so requiring the full stripped line to equal the + # signature (not a substring) keeps a check that merely *prints* the + # literal mid-line — e.g. pytest assertion introspection of egg's own + # green-gate tests — from tagging itself infra and failing its own red + # open (#3417 review). + stripped_lines = None + for sig in infra_line_signatures: + if stripped_lines is None: + stripped_lines = {ln.strip() for ln in out.splitlines()} + if sig in stripped_lines: + return sig + # Substring match for the kernel ENOSPC strerror, which surfaces + # embedded in a larger message rather than on its own line. + for sig in infra_substring_signatures: if sig in out: return sig return None @@ -414,7 +467,12 @@ def _build_runner_job_manifest( full_env["EGG_GREEN_GATE_CHECKS"] = json.dumps(checks) full_env["EGG_GREEN_GATE_REPO_DIR"] = repo_dir full_env["EGG_GREEN_GATE_OUTPUT_TAIL"] = str(_VERDICT_OUTPUT_TAIL_CHARS) - full_env["EGG_GREEN_GATE_INFRA_SIGNATURES"] = json.dumps(list(_INFRA_OUTPUT_SIGNATURES)) + full_env["EGG_GREEN_GATE_INFRA_SIGNATURES"] = json.dumps( + { + "line": list(_INFRA_LINE_SIGNATURES), + "substring": list(_INFRA_SUBSTRING_SIGNATURES), + } + ) volumes = [] volume_mounts = [] diff --git a/orchestrator/tests/test_slice_green_gate.py b/orchestrator/tests/test_slice_green_gate.py index b0f33f0bd3..66b2be9de3 100644 --- a/orchestrator/tests/test_slice_green_gate.py +++ b/orchestrator/tests/test_slice_green_gate.py @@ -244,7 +244,12 @@ def _run_runner( "EGG_GREEN_GATE_CHECKS": json.dumps(checks), "EGG_GREEN_GATE_REPO_DIR": str(repo_dir), "EGG_GREEN_GATE_OUTPUT_TAIL": "200", - "EGG_GREEN_GATE_INFRA_SIGNATURES": json.dumps(list(sgg._INFRA_OUTPUT_SIGNATURES)), + "EGG_GREEN_GATE_INFRA_SIGNATURES": json.dumps( + { + "line": list(sgg._INFRA_LINE_SIGNATURES), + "substring": list(sgg._INFRA_SUBSTRING_SIGNATURES), + } + ), "EGG_GREEN_GATE_REQUIRE_PREBUILT": require_prebuilt, "EGG_GREEN_GATE_PREBUILT_BASE": str( prebuilt_base if prebuilt_base is not None else tmp_path / "no-prebuilt" @@ -370,6 +375,63 @@ def test_signature_scrolled_out_of_tail_is_still_detected(self, tmp_path: Path) assert "GATEWAY SIDECAR NOT AVAILABLE" not in check["output_tail"] assert check["infra"] == "GATEWAY SIDECAR NOT AVAILABLE" + def test_signature_printed_midline_is_not_tagged(self, tmp_path: Path) -> None: + # The #3417-review self-masking guard: egg's own green-gate tests + # contain these literals, so a genuine regression prints them via + # pytest assertion introspection — always mid-line, behind an + # ``E``/``assert``/quote prefix. Whole-line matching must NOT tag + # such output as infra, or the gate would fail its own red open. + proc = _run_runner( + tmp_path, + [ + { + "name": "regressed-green-gate-test", + "command": ( + "echo \"E assert None == 'GATEWAY SIDECAR NOT AVAILABLE'\"; exit 1" + ), + } + ], + ) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + check = verdict["checks"][0] + assert check["ok"] is False + assert check["infra"] is None + + def test_indented_signature_line_is_tagged(self, tmp_path: Path) -> None: + # sandbox/scripts/git emits GATEWAY SIDECAR NOT AVAILABLE inside a + # banner with leading whitespace; whole-line matching strips the + # line before comparing, so the real fault still tags. + proc = _run_runner( + tmp_path, + [ + { + "name": "infra-red-banner", + "command": "printf ' GATEWAY SIDECAR NOT AVAILABLE\\n'; exit 1", + } + ], + ) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + assert verdict["checks"][0]["infra"] == "GATEWAY SIDECAR NOT AVAILABLE" + + def test_enospc_is_tagged_as_substring_midline(self, tmp_path: Path) -> None: + # ENOSPC surfaces embedded in a larger strerror, so it matches as + # a substring (not whole-line) — disk pressure is infra either way + # (#3417 review). + proc = _run_runner( + tmp_path, + [ + { + "name": "disk-red", + "command": "echo 'OSError: [Errno 28] No space left on device'; exit 1", + } + ], + ) + verdict = sgg.parse_verdict(proc.stdout) + assert verdict is not None + assert verdict["checks"][0]["infra"] == "No space left on device" + def test_prebuilt_restore_copy_if_missing(self, tmp_path: Path) -> None: prebuilt = tmp_path / "prebuilt" / "jwbron--egg" (prebuilt / ".venv" / "bin").mkdir(parents=True) @@ -471,9 +533,10 @@ def test_env_carries_infra_signatures(self) -> None: e["name"]: e["value"] for e in manifest["spec"]["template"]["spec"]["containers"][0]["env"] } - assert json.loads(env["EGG_GREEN_GATE_INFRA_SIGNATURES"]) == list( - sgg._INFRA_OUTPUT_SIGNATURES - ) + assert json.loads(env["EGG_GREEN_GATE_INFRA_SIGNATURES"]) == { + "line": list(sgg._INFRA_LINE_SIGNATURES), + "substring": list(sgg._INFRA_SUBSTRING_SIGNATURES), + } def test_env_carries_checks_and_repo_dir(self) -> None: manifest = _manifest() From 3b6c26faf686fbe0fedccf1f87255677c203f213 Mon Sep 17 00:00:00 2001 From: jwbron <8340608+jwbron@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:07:33 +0000 Subject: [PATCH 3/4] Fix checks: point artifact-spec prompt test at pipelines package pipelines.py was decomposed into the routes/pipelines/ package; the agent-facing prompt builders now live in its _prompt_*.py submodules. TestConsistencyC_PromptDerivesFromSpec still read the removed monolith path, so the readable/no-literal/resolve-call invariants errored with FileNotFoundError. Scan the concatenated _prompt_*.py submodules instead. --- .../egg_contracts/tests/test_artifact_spec.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/shared/egg_contracts/tests/test_artifact_spec.py b/shared/egg_contracts/tests/test_artifact_spec.py index 3caec9f87e..ab98a9fd8c 100644 --- a/shared/egg_contracts/tests/test_artifact_spec.py +++ b/shared/egg_contracts/tests/test_artifact_spec.py @@ -11,11 +11,12 @@ (:class:`gateway.phase_filter.PhaseFilter`); * the gateway-mirror in the sandbox (:func:`egg_restrictions.phase_patterns.phase_file_verdict`); -* the orchestrator's ``_get_draft_path`` helper in - ``orchestrator/routes/pipelines.py`` (used by refine / plan +* the orchestrator's ``_get_draft_path`` helper in the + ``orchestrator/routes/pipelines/`` package (used by refine / plan propose validation in ``orchestrator/routes/signals.py``); and -* the prompt f-string literals in ``orchestrator/routes/pipelines.py`` - that name draft / agent-output paths to the agent. +* the prompt f-string literals in the ``_prompt_*.py`` submodules of + ``orchestrator/routes/pipelines/`` that name draft / agent-output + paths to the agent. This module is the refine-risk-1 mitigation from the #3077 plan: any future drift in any of these replicas fails CI here instead of @@ -408,10 +409,19 @@ class TestConsistencyC_PromptDerivesFromSpec: (covered by Consistency-B above). """ - PIPELINES_PATH = ( - Path(__file__).resolve().parents[3] / "orchestrator" / "routes" / "pipelines.py" + # ``pipelines.py`` was decomposed into the ``routes/pipelines/`` + # package (decomposition pattern, #3312); the agent-facing prompt + # builders now live in its ``_prompt_*.py`` submodules. The invariants + # below scan the concatenation of those submodules — the successor to + # the pre-split monolith's prompt-construction code. + PROMPT_BUILDER_DIR = ( + Path(__file__).resolve().parents[3] / "orchestrator" / "routes" / "pipelines" ) + @classmethod + def _prompt_builder_files(cls) -> list[Path]: + return sorted(cls.PROMPT_BUILDER_DIR.glob("_prompt_*.py")) + # Ratchet against a regression: forbid raw # ``.egg-state/agent-outputs/{_identifier}-…`` f-string literals # from creeping back into pipelines.py once the slice-3 rewrite has @@ -426,10 +436,14 @@ class TestConsistencyC_PromptDerivesFromSpec: @pytest.fixture(scope="class") def pipelines_text(self) -> str: - return self.PIPELINES_PATH.read_text() + return "\n".join(p.read_text() for p in self._prompt_builder_files()) def test_pipelines_py_is_readable(self) -> None: - assert self.PIPELINES_PATH.exists(), f"missing: {self.PIPELINES_PATH} — has the file moved?" + files = self._prompt_builder_files() + assert files, ( + f"no prompt-builder submodules (_prompt_*.py) found under " + f"{self.PROMPT_BUILDER_DIR} — has the pipelines package moved?" + ) def test_no_raw_agent_output_literals_remain(self, pipelines_text: str) -> None: # Slice-3 of #3077 removed every From 9b419b12857c92e9c550c42730ebde25426599ea Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:27:07 +0000 Subject: [PATCH 4/4] Sweep stale pipelines.py references in test_artifact_spec.py The prompt-construction code TestConsistencyC scans was decomposed from pipelines.py into the routes/pipelines/ package (#3312), but the test's docstrings and assertion messages still referred to pipelines.py. Repoint them at the package and rename test_pipelines_py_is_readable to test_pipelines_package_is_readable to match what it now checks. --- .../egg_contracts/tests/test_artifact_spec.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/shared/egg_contracts/tests/test_artifact_spec.py b/shared/egg_contracts/tests/test_artifact_spec.py index 1f0db1021f..91dd8e39c6 100644 --- a/shared/egg_contracts/tests/test_artifact_spec.py +++ b/shared/egg_contracts/tests/test_artifact_spec.py @@ -386,8 +386,9 @@ def test_get_human_draft_path_none_for_unregistered_phase(self) -> None: class TestConsistencyC_PromptDerivesFromSpec: - """The agent-facing prompts in pipelines.py derive their agent-output - paths via :func:`resolve_artifact_path` instead of inlining literals. + """The agent-facing prompts in the ``pipelines/`` package derive their + agent-output paths via :func:`resolve_artifact_path` instead of inlining + literals. Pre-slice-3 of #3077 this test asserted that the prompt f-string literals (``f".egg-state/agent-outputs/{_identifier}-architect-output.json"``) @@ -401,8 +402,9 @@ class TestConsistencyC_PromptDerivesFromSpec: This test pins the new invariant: every registered ``agent-outputs/`` spec must appear as a ``resolve_artifact_path("", …)`` - call in pipelines.py, and no literal ``.egg-state/agent-outputs/{_identifier}-…`` - string may sneak back in (the ratchet against #3016-style drift). + call somewhere in the ``pipelines/`` package, and no literal + ``.egg-state/agent-outputs/{_identifier}-…`` string may sneak back in + (the ratchet against #3016-style drift). Drafts under ``.egg-state/drafts/`` are constructed via ``_get_draft_path``, which itself routes through the spec @@ -418,8 +420,9 @@ class TestConsistencyC_PromptDerivesFromSpec: # Ratchet against a regression: forbid raw # ``.egg-state/agent-outputs/{_identifier}-…`` f-string literals - # from creeping back into pipelines.py once the slice-3 rewrite has - # landed. The check intentionally matches only the templated form + # from creeping back into the ``pipelines/`` package once the + # slice-3 rewrite has landed. The check intentionally matches only + # the templated form # (with the literal ``{_identifier}`` placeholder); resolved paths # appearing in test fixtures or doc strings — e.g. a sample # ``3077-architect-output.json`` — would not match and remain free @@ -435,7 +438,7 @@ def pipelines_text(self) -> str: # the decomposition. return "\n".join(path.read_text() for path in sorted(self.PIPELINES_PATH.glob("*.py"))) - def test_pipelines_py_is_readable(self) -> None: + def test_pipelines_package_is_readable(self) -> None: assert self.PIPELINES_PATH.is_dir(), ( f"missing: {self.PIPELINES_PATH} — has the package moved?" ) @@ -452,7 +455,7 @@ def test_no_raw_agent_output_literals_remain(self, pipelines_text: str) -> None: # here is the slice-1 / slice-2 #3016-style drift symptom. offenders = self._BANNED_LITERAL_RE.findall(pipelines_text) assert not offenders, ( - "pipelines.py reintroduced raw agent-output path literals; " + "the pipelines/ package reintroduced raw agent-output path literals; " "use resolve_artifact_path(, identifier) instead so the " "spec stays the single source of truth: " f"{sorted(set(offenders))!r}" @@ -465,7 +468,8 @@ def test_every_agent_output_spec_has_resolve_call( ) -> None: # Reverse direction: every registered ``agent-outputs/`` spec # must appear as a ``resolve_artifact_path("", …)`` call - # in pipelines.py. Drafts under ``.egg-state/drafts/`` are + # somewhere in the ``pipelines/`` package. Drafts under + # ``.egg-state/drafts/`` are # constructed via ``_get_draft_path`` (Consistency-B above), so # this only governs the ``agent-outputs/`` rows. for spec in all_specs: @@ -480,8 +484,8 @@ def test_every_agent_output_spec_has_resolve_call( ) assert any(n in pipelines_text for n in needles), ( f"{spec.name}: expected `resolve_artifact_path(, …)` " - f"call in pipelines.py — drift between spec and prompt " - f"rendering will silently land at the agent" + f"call in the pipelines/ package — drift between spec and " + f"prompt rendering will silently land at the agent" )