select_tests: bare-name AST resolver, empty-diff skip, drop canary - #2262
Conversation
Three changes to `scripts/select_tests.py` driven by inner-loop friction: 1. **Bare-name AST resolver.** The codebase universally uses bare-name imports (verified 406/407 test files and 33/33 sampled production files), but grimp registers production under fully-qualified names. Without help, grimp's `find_downstream_modules` returns the empty set for almost every source-code change and the `no downstream tests for changed module` widening trigger fires for nearly every diff — narrowing was effectively inert. The resolver AST-scans every module in the graph, maps each bare-name import target back to its fully-qualified production module via the same prefix-stripping rules grimp's sys.path injections imply (`shared.`, `orchestrator.`, `sandbox.`, `sandbox.tools.`), and stores a reverse-edge map on `GraphBundle`. `reverse_closure` now unions grimp's transitive closure with these synthetic edges, and the zero-downstream-offender check consults the union — so the trigger only fires on a true blind spot. End-to-end: editing `orchestrator/action_guards.py` now selects 106/278 tests (the actual transitive consumers); previously this widened to all 278. Tracked separately: - #2256 — assess rewriting bare-name imports to package-prefix paths (alternative cleanup path; resolver is the contained internal fix). - #2259 — top-level `tests/` tree shadowed by `sandbox/tests/` during graph build, blocking narrowing for tests under that subtree (pre-existing issue, surfaced during verification). 2. **Empty-diff short-circuit.** An empty diff against a resolvable, current baseline now logs `selected 0 tests (skipping pytest)` and exits with empty stdout — the Makefile already keys off empty selector output to skip pytest entirely. Previously the selector widened to the full suite via an `empty diff` trigger, so `make test` on a clean tree spent minutes running every test for no reason. Unresolvable-baseline / stale-LKG still widen for safety. 3. **Drop the canary.** The every-10th-invocation full-suite tick was defense in depth against unknown static-analysis blind spots. With the bare-name resolver in place the analysis is much more thorough, and the remaining true blind spots (subprocess crossings, data-file loads, dynamic imports) are already covered by their own explicit triggers. The cost — every 10th `make test` blowing up to a full suite — is no longer worth the implicit insurance. Ground truth is `make test-all` before push (which CI runs anyway). Test counts: 302 selector tests pass (was 323; the delta is the 9 canary tests deleted plus 12 unrelated parametrize cases removed from the canary trigger row, replaced with the new resolver suite — 32 tests in `test_select_tests_bare_name.py`).
There was a problem hiding this comment.
Review
The bare-name AST resolver is a well-targeted fix for the codebase's structural
mismatch with grimp. The new test coverage is solid, the resolver fails open on
the right axes (SyntaxError / OSError / ValueError), and the integration with
both reverse_closure and the zero-downstream-offender check is clean. I have
one blocking issue and a handful of non-blocking observations.
Blocking
1. Empty-diff short-circuit silently breaks make test PYTEST_ARGS=… on a clean tree.
scripts/select_tests.py:1479 now early-returns when diff is empty:
if not diff and baseline_source != "UNRESOLVABLE" and not lkg_was_stale:
... return 0The PYTEST_ARGS bypass classifier (bypass_narrowing at line 1526) lives
after this short-circuit. Before this PR, an empty diff fell through to
evaluate_fallback_triggers, which returned "empty diff" — but the bypass
check ran first and would write mode=bypass in the selection record. The
Makefile (Makefile:308) keys off mode=bypass to actually invoke pytest with
the user's PYTEST_ARGS.
After this PR, on a clean tree:
- Selector hits the empty-diff branch, writes
mode=narrow, returns 0 with
empty stdout. - Makefile sees empty stdout, checks selection record, finds
mode=narrow
(notbypass), printsselect-tests: no tests selected, exits 0. - The user's
PYTEST_ARGS=tests/foo/test_bar.pyis silently ignored. Pytest
never runs.
The PR's own docs (docs/guides/testing.md:454) still promise: "An explicit
path in PYTEST_ARGS bypasses narrowing — the developer is asking for that
specific selection." That contract is now violated for the very common case
of running a single test on a clean tree (e.g., re-running a passing test
after rebasing onto an LKG).
The new e2e test test_empty_diff_subprocess_skips_pytest
(tests/tools/test_select_tests_fallbacks.py:404) doesn't exercise this
combination — it never sets PYTEST_ARGS_RAW, so the regression doesn't
show up in CI.
Fix: either move the empty-diff short-circuit to after the PYTEST_ARGS
bypass check (line 1557), or include the same pytest_args_have_explicit_path
guard inside the empty-diff branch. Add a regression test that sets
PYTEST_ARGS_RAW=tests/... on a clean-tree fixture and asserts pytest is
invoked (or the selection record records mode=bypass).
Non-blocking
2. _extract_imports doesn't yield the parent package for import X.Y.
scripts/select_tests.py:735-738: for ast.Import nodes, only the dotted
name as written is added (X.Y.Z), not the intermediate parents. Python
actually loads X, X.Y, AND X.Y.Z at runtime, so a test that only does
import egg_logging.signatures should be linked to changes in
shared/egg_logging/__init__.py too.
Today this mostly self-heals because the more idiomatic from egg_logging import signatures (line 744-747) already yields both egg_logging and
egg_logging.signatures, and the zero-downstream-offender check widens to
full suite when an __init__.py change has no recorded consumers. But in a
mixed diff where one module has consumers and another doesn't, this can
under-narrow silently.
Two-line fix: for ast.Import, also walk the dotted prefixes and add each
one. The existing tests (test_extract_imports_dotted_import) only assert
the leaf is captured — extend them to assert prefixes too.
3. Test test_upstream_edges_handles_missing_file is too permissive.
tests/tools/test_select_tests_bare_name.py:1056:
assert "orchestrator.action_guards" in edges or edges == {}The or edges == {} arm makes this a tautology — the test passes even if
build_bare_name_upstream_edges returns an empty dict and silently does no
work. With a valid file plus a missing-file module in the same all_modules,
the assertion should be definite: the valid file's edge IS recorded AND the
missing module produced no key. Tighten to a single conjunction.
4. The intra-prefix-loop comment in build_bare_name_index is contradictory.
scripts/select_tests.py:775-780 says "should NOT also surface as tools.foo
AND foo" and then says "but in practice we want both views" and "Continue
the loop." The implementation does record both, which is the right call given
the over-inclusion-is-safer policy. Rewrite the comment to state the policy
once: every applicable prefix view is recorded; ambiguity is intentional.
5. AST scan cost is unbounded by the cache.
build_bare_name_upstream_edges re-AST-parses every file in all_modules
on every selector invocation. The grimp graph itself is cached
(GRIMP_CACHE_DIR) but the bare-name reverse map is not. With ~440 files
in the repo today this is fine (~1-2s), but the linear-in-repo-size cost
will erode as the codebase grows. Worth a follow-up to either persist the
map alongside the grimp cache or memoize per file mtime.
6. Loss of canary defense-in-depth is a real trade-off.
The PR argues the bare-name resolver makes the every-10th-invocation full-
suite tick redundant. The resolver covers bare-name imports but the canary
covered unknown blind spots — patterns we haven't taught any analyzer
about. The remaining named blind spots (subprocess crossings, data-file
loads, dynamic imports) are explicit triggers, but those only fire when a
known sentinel is in the diff. If a future change introduces a novel
runtime-only coupling, narrow make test will silently miss it until
someone runs make test-all before push.
The trade-off (faster inner loop vs. catching unknown unknowns) is
defensible, but I'd push for keeping a less-frequent canary (every 25th or
50th invocation?) rather than dropping it entirely. Not blocking — the PR
leans on make test-all before push as the safety net, which is the
documented contract.
Verification done
- Walked the empty-diff regression by hand against
Makefile:296-329and
confirmedmode=narrow(notmode=bypass) is what the empty-diff branch
writes. - Confirmed
is_role_readonly,read_sidecar_lkg,_canary_pathremoval
is consistent — no dangling references in production code or docs (the
remaining mentions are in.egg-state/BRC history, which is gitignored
pipeline artifacts). - Verified
BARE_NAME_STRIP_PREFIXESmatches the sys.path injections in
build_graph(scripts/select_tests.py:916-923);gateway/is
correctly absent. - Reviewer #1 fallback ((c) zero-downstream-offender widening) now correctly
uses_walk_upstream_combined, so a real bare-name path through tests
prevents spurious widening.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
… prefixes Address review feedback on PR #2262: 1. Blocking: empty-diff short-circuit silently broke 'make test PYTEST_ARGS=...' on a clean tree. The bypass classifier ran AFTER the short-circuit, so an explicit user path was dropped (Makefile saw mode=narrow and skipped pytest). Reorder so the PYTEST_ARGS bypass check runs first; the bypass also avoids the now-skipped graph build. Adds in-process regression test plus an end-to-end subprocess case. 2. _extract_imports now yields every dotted prefix of 'import a.b.c' (a, a.b, a.b.c) so a change to a parent package's __init__.py reaches importers of a deeper submodule. Existing dotted-import test asserts both prefix and leaf. 3. test_upstream_edges_handles_missing_file tightened: was 'in edges or edges == {}' (tautology); now asserts a peer file's edge IS recorded and the missing module produced no key. 4. build_bare_name_index comment rewritten to state the policy once (every applicable prefix view recorded; ambiguity is intentional for safer over-inclusion).
There was a problem hiding this comment.
Thanks for the thorough review. Addressed all four actionable items in 41a81a3:
1. Blocking — empty-diff vs PYTEST_ARGS. Reordered so the bypass classifier runs first (scripts/select_tests.py:1467-1518). The bypass branch also no longer builds the graph — bypass ignores the closure entirely, so total_count falls back to len(TEST_ROOT_DIRS). Added two regression tests:
test_pytest_args_bypass_takes_precedence_over_empty_diff(in-process, stubbed_run_git) — confirmed it fails on the un-fixed code withmode=narrowand passes on the fix withmode=bypass.test_empty_diff_with_pytest_args_explicit_path_takes_bypass(subprocess, mirrors the existing empty-diff e2e shape).
2. _extract_imports prefix walk. import a.b.c now yields {a, a.b, a.b.c} so a change to a parent package's __init__.py reaches importers of a deeper submodule. Updated test_extract_imports_dotted_import to assert both leaf and parent; added test_extract_imports_deeply_dotted_import_yields_all_prefixes for three-level coverage.
3. Tightened test_upstream_edges_handles_missing_file. Now asserts a peer file's edge IS recorded AND the missing module produced no key — no more or edges == {} tautology.
4. Rewrote the build_bare_name_index comment to state the policy once: every applicable prefix-stripped view is recorded; ambiguity is intentional for safer over-inclusion.
Non-blocking #5 (AST scan cost): Agreed it's worth tracking. Filing as a follow-up rather than addressing in this PR — the persistence boundary touches GRIMP_CACHE_DIR invalidation and is a separable change.
Non-blocking #6 (canary trade-off): Disagree on keeping a less-frequent canary. The argument for dropping it isn't that the AST resolver covers everything — it's that an unpredictable every-Nth-invocation full-suite tick is a poor signal regardless of N. Whether N=10 or N=50, the developer who happens to land on the tick eats minutes of full-suite latency for an unrelated reason, and the developer who narrowly avoids it gets the same blind-spot exposure with no warning. make test-all before push is the documented safety net, and CI's make test-all is the ground truth. A periodic-tick canary muddies the inner loop without changing the safety story. Happy to revisit if we see a concrete novel-coupling miss in practice.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
All four actionable items from my previous review are addressed correctly. The bypass-vs-empty-diff ordering fix is the right shape, and the regression coverage is solid (both an in-process test driving _run_narrow_or_fallback directly with a stubbed _run_git, and a subprocess test against a real synthetic git repo).
Verification
Blocking #1 — bypass before empty-diff (FIXED). The bypass classifier now lives at scripts/select_tests.py:1488-1532, ahead of the empty-diff branch at :1539. The bypass branch correctly skips build_graph (the closure isn't consulted in bypass mode) and writes mode=bypass so the Makefile's Makefile:308 grep keys correctly. Walked the in-process test (tests/tools/test_select_tests_fallbacks.py:117-180) and the subprocess test (:525-593) — both exercise the empty-diff + explicit-path collision and assert mode=bypass rather than mode=narrow.
Non-blocking #2 — dotted-import prefix walk (FIXED). _extract_imports at scripts/select_tests.py:740-750 now emits every prefix for ast.Import (import a.b.c → {a, a.b, a.b.c}), matching the import machinery's actual behavior. test_extract_imports_dotted_import now asserts both leaf + parent prefix; test_extract_imports_deeply_dotted_import_yields_all_prefixes covers three-level. Index lookups for non-package prefixes (e.g., os in import os.path) silently miss when nothing in all_modules matches, so this can't over-include.
Non-blocking #3 — tightened missing-file test (FIXED). test_upstream_edges_handles_missing_file (tests/tools/test_select_tests_bare_name.py:282-306) now adds orchestrator/api.py as a peer importing the present file, and asserts a definite conjunction: "orchestrator.api" in edges["orchestrator.action_guards"] AND "orchestrator.does_not_exist" not in edges. The or edges == {} tautology is gone.
Non-blocking #4 — comment rewrite (FIXED). build_bare_name_index comment at :782-789 now states the over-inclusion policy positively without contradicting itself.
Non-blocking #5 (AST scan cost): Reasonable to defer to a follow-up — the persistence boundary is a separable concern.
Non-blocking #6 (canary): The disagreement is well-reasoned. The "every-Nth-invocation full suite" pattern is poor signal regardless of N — agreed.
One non-blocking observation on the new code
Bypass mode's total_count=len(TEST_ROOT_DIRS) (scripts/select_tests.py:1523) records 4 (the number of test root dirs) when the actual repo has ~302 tests. This is purely cosmetic in the telemetry record — nothing reads it functionally — and matches the pattern at :1469 for the cannot-resolve-HEAD path. Worth noting for anyone analyzing selection records by hand. Not blocking.
What I didn't re-check
The unchanged pieces (resolver core, prefix table, fallback evaluator, end-to-end on a real diff) — all green in the prior review.
LGTM, approving.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
|
Thanks for the re-review and approval. On the non-blocking observation about The bypass branch deliberately skips If we ever want a more accurate denominator for offline analysis of selection records, the right move is probably a separate post-hoc enrichment pass over the records (which has access to — Authored by egg |
|
egg feedback addressed. View run logs 5 previous review(s) hidden. |
orchestrator/routes/pipelines.py grew from 15356 to 15593 lines due to the post-timeout rebaseline changes in this PR. orchestrator/peer_consensus.py grew from 1988 to 2003 lines. scripts/select_tests.py grew from 1650 to 1850 lines due to changes merged to main in PR #2262 (bare-name AST resolver, empty-diff skip). Update all three baselines so the file-size lint passes in CI.
…snapshot - orchestrator/mcp_tools.py: 2817→2818 lines (docstring edit in this PR) - orchestrator/peer_consensus.py: 1988→2003 lines (grown on main) - orchestrator/routes/pipelines.py: 15356→15514 lines (grown on main) - scripts/select_tests.py: 1650→1850 lines (bare-name AST resolver, #2262)
…2271) * docs: update testing.md for bare-name AST resolver Document the bare-name AST resolver added in #2262, which supplements grimp for orchestrator/sandbox/shared bare-name imports. Authored-by: egg * Fix file-size allowlist baselines for files grown since last update Update baselines for orchestrator/peer_consensus.py, orchestrator/routes/pipelines.py, and scripts/select_tests.py to reflect their current sizes and unblock the Custom Checks lint job. * Tighten bare-name AST scope in testing guide Per review feedback: 'every in-repo .py' overstated coverage. The AST resolver iterates bundle.graph.modules, which is limited to files registered via PACKAGES. Clarify that scripts/, integration_tests/, and other un-registered trees are not scanned. --------- Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com> Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
…ess (#2253) * Fix #2245: rebaseline post-consensus-timeout budget on producer progress The hardcoded `post_timeout_budget = 3600` in `_run_concurrent_phase` counted iteration time against a single fixed bucket, so a healthy multi-iteration BRC consensus cycle (NACK → repropose → re-review) could be force-killed mid-iteration even when reviewers were actively producing useful feedback. Replace it with a per-iteration clock that rebaselines on producer progress: each fresh CONSENSUS_PROPOSE (initial or NACK→re-propose) resets the iteration budget so the next round of reviews gets a clean clock instead of inheriting the prior round's wall-clock spend. An absolute cap (`post_consensus_max_total_seconds`, default 4h) bounds the total wait so unbounded propose churn can't stall the pipeline. Both budgets are exposed on `PipelineConfig` for per-deployment tuning. Defaults (3600s per-iteration / 14400s absolute) preserve the prior force-kill point for the no-progress case. * Address review feedback on PR #2253 - Drop dead `TypeError` fallback in `_latest_proposal_ts`. The `get_peer_consensus_tracker(pipeline_id, slice_id=None)` signature cannot raise `TypeError` for a positional `slice_id`; the fallback was speculative and unreachable. - Read the new post-consensus knobs directly off `PipelineConfig` instead of `getattr(..., default)`. Pydantic provides defaults; the `getattr` defaults silently masked any future field rename. - Add a cross-field validator on `PipelineConfig` that rejects `post_consensus_max_total_seconds < post_consensus_iteration_budget_seconds`. Without it, a misconfigured pipeline silently makes the per-iteration rebaseline logic unreachable (the absolute cap fires first every time). - Replace the `__dict__` fallback in the test helper with `PipelineConfig(**overrides)` — the fallback was dead code and would have bypassed Pydantic validation if it ever fired. - Tighten the post-timeout-snapshot comment: the safety against `datetime > None` comes from the `is None` short-circuit at the rebaseline check, not from any datetime/None ordering. - Add tests for the new cross-field validator (rejects mismatched budgets, accepts equal budgets). * Address review feedback: pin absolute-cap warning + silence gate fallback - test_absolute_cap_bounds_unbounded_proposal_churn now patches routes.pipelines.logger and asserts the warning message contains 'absolute cap reached' (and that 'iteration budget exhausted' did not fire). With iteration_budget == max_total the prior assertion (exit_code == 1) couldn't distinguish the two caps; this pins the branch so an off-by-one moving the per-iteration check above the rebaseline would now fail loudly. - Both rebaseline tests now stub mock_tracker.get_latest_progress_ timestamp.return_value = None so the pre-timeout progress gate (#2243) doesn't hit its exception-fallback path on the auto-attribute MagicMock and produce noisy 'BRC progress-gate tracker check failed' WARN logs that would mask a real gate-side regression. Both items called out as non-blocking suggestions in the egg-reviewer re-review of e5055e5. * Fix Custom Checks: update file-size allowlist baselines for PR #2253 orchestrator/routes/pipelines.py grew from 15356 to 15593 lines due to the post-timeout rebaseline changes in this PR. orchestrator/peer_consensus.py grew from 1988 to 2003 lines. scripts/select_tests.py grew from 1650 to 1850 lines due to changes merged to main in PR #2262 (bare-name AST resolver, empty-diff skip). Update all three baselines so the file-size lint passes in CI. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com>
…e-size-allowlist.yaml Both sides bumped baselines for orchestrator/routes/pipelines.py and scripts/select_tests.py. After the merge, the actual file sizes are 15594/681452 (pipelines.py) and 1875/75206 (select_tests.py), so the baselines are set to those values. Brings in main's progress-gate (#2254), post-timeout rebaseline (#2253), select_tests AST resolver (#2262/#2266), and max-file-size lint (#2250).
…mpt fix (#2260) * Fix #2249: tester scaffold-first telemetry + producer-orientation prompt fix Closes the gap between the existing scaffold-first instruction and the observed behavior on pipeline issue-1557-v2 where tester polled wait-loop for 44 minutes without writing any test scaffolding. Two changes: 1. Producer-orientation prompt (orchestrator/routes/pipelines.py:8654) The scaffold-first directive previously lived only in the reviewer-preparation block, while wait-loop is the comfort path on the producer side. Mirror the directive into the producer-orientation block — including an explicit "do NOT call wait-loop before drafting scaffolds" line — so the instruction sits adjacent to the path that was pulling tester away from it. Babysit/PR mode is unaffected (early-return at line 8643). 2. Telemetry script (scripts/scaffold_first_telemetry.py) Walks .egg-state/brc-history/*-implement.json, finds coder's first CONSENSUS_PROPOSE, and matches scaffold keywords (scaffold/test file/drafted/prepared test/fixture/signature/stub) against tester heartbeat bodies in the wait window. Reports per-pipeline rows and the aggregate scaffold-first fraction. Heartbeat-body matching is a proxy for direct tool-call telemetry; script docstring documents the false-negative risk. On the existing 18 implement-phase BRC histories the fraction is 38.9% — below the issue's 50% "structurally weak prompt" threshold, which justifies shipping change (1) alongside the telemetry rather than waiting for telemetry-first. * Address review feedback on #2249 telemetry script - Tighten weak keywords: \bdrafted\b -> \bdrafted (test|scaffold|fixture) and \bsignature -> \btest signature so unrelated phrasing like 'drafted plan' or 'method signature changed in dep' does not fire. - Use statistics.median for strict-median semantics (was index-based high-median for even-length samples). - Parse timestamps before comparing in _heartbeats_before so Z-suffixed values sort correctly against +00:00 cutoffs. - Add tests for the default text-output path (header + summary line), --verbose excerpts, Z-suffix timestamp handling, and drafted/signature false-positive rejection. Verified against the 18 existing implement BRC histories: still 7/18 (38.9%) with tightened keywords (the positive matches all hit through scaffold/fixture/test file/stub). Median moves from high-median to true median (23.33 vs prior figure). * Sync scaffold_first_telemetry docstring with tightened keyword set The module docstring still listed the pre-tightening keyword set ("drafted/.../signature"). dc9f6e0 tightened those to "drafted (test|scaffold|fixture)" and "test signature" in _SCAFFOLD_KEYWORDS but only updated the inline comment. Update the docstring to match and point readers at the source list. * Update file-size allowlist baselines for peer_consensus and pipelines * Fix file-size allowlist: update select_tests.py baseline after #2262 #2262 grew scripts/select_tests.py from 1650→1850 lines but the allowlist baseline was not updated to match. Update baseline to (1850 lines, 73711 bytes) and attribute to issue #2262. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com>
…#2257) * Fix #2255: replace asyncio.get_event_loop() with asyncio.run() on Python 3.14 `asyncio.get_event_loop()` no longer auto-creates a loop in Python 3.14 when none is running, raising RuntimeError. Swap to `asyncio.run()`, which manages its own loop lifecycle — the behavior the helper actually wants. CI runs on 3.13 and didn't surface this; the failure reproduces locally on 3.14. * Pin Python to 3.14 across CI, pyproject, and runtime images CI was on 3.13 while local dev is on 3.14, which is how #2255 slipped past CI. Pin everything to 3.14 so the supported version is what gets tested: - CI workflows (test, test-e2e, test-integration, lint): 3.13 → 3.14 - Root pyproject.toml: requires-python, ruff target-version, mypy python_version → 3.14 - Sub-package pyprojects (sandbox, shared, egg_contracts, egg_harness): loose >=3.11 lower bound → >=3.14 to match the actual supported floor - Runtime images (orchestrator, gateway, sandbox): python:3.13-slim → 3.14-slim; sandbox deadsnakes apt installs python3.14 * Fix checks: apply automated formatting fixes * Address review: re-quote unsafe forward refs and update docs Restore string quotes on annotations whose referenced names exist only under TYPE_CHECKING or inside function-body imports — UP037 had unstrung these but they still need to be forward references at runtime even on PEP-649 (3.14): - orchestrator/mcp_tools.py:_get_gateway_client (GatewayClient) - orchestrator/routes/__init__.py:get_state_store_for_pipeline (StateStore, Pipeline) - orchestrator/routes/pipelines.py (ContainerSpawner, MountSpec) - orchestrator/kubernetes_spawner.py (MountSpec) Each restored string is paired with a per-line `# noqa: UP037` so ruff stops re-applying the autofix while leaving UP037 on for the rest of the codebase. Update docs that still claimed Python 3.11/3.13 to 3.14: - README.md (top-level Python requirement) - skills/egg-setup/SKILL.md (minimum + sample output) - orchestrator/README.md (Dockerfile slim base) - docs/architecture/slice-dag.md (ruff target-version reference) * Address review nits: PEP 695 generics and stale 3.x comments - gateway/{gateway,auth,mode_gate}.py: convert F TypeVar to PEP 695 generic function syntax. Removes the per-decorator # noqa: UP047 overrides whose rationale ('Python 3.11 compat') is moot now that the floor is 3.14. - shared/egg_contracts/dependency_graph.py: comment said 'Python 3.13 target' for the PEP-695 generic class; updated to 3.14 to match pyproject.toml. - orchestrator/mcp_tools.py: _is_timeout_error docstring rewritten to drop the dead 3.11 branch reference and explain why the explicit socket.timeout check is kept on 3.14 (urllib URLError.reason wrapping). * Fix file-size allowlist: update baselines for files grown since last snapshot - orchestrator/mcp_tools.py: 2817→2818 lines (docstring edit in this PR) - orchestrator/peer_consensus.py: 1988→2003 lines (grown on main) - orchestrator/routes/pipelines.py: 15356→15514 lines (grown on main) - scripts/select_tests.py: 1650→1850 lines (bare-name AST resolver, #2262) * Fix UP037: remove quotes from PipelineConfig return type annotation --------- Co-authored-by: egg <egg@localhost> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com>
… post-ACK threshold (#2268) * Fix #2242: alive-signal gate on heartbeat/progress alerts; plan-phase post-ACK threshold Heartbeat-stall and progress-stall alerts fired prematurely on plan-phase producers during long-form Anthropic completions: the agent is mid-draft, no tool calls in flight, so no `mcp__brc__send_heartbeat` arrives on the bus. On `issue-1557-v2` this escalated to a 3-of-3 producer-silence alert at 355s while every producer was simply composing its draft. Separately, the 180s post-ACK confirmation timeout was tight for plan-phase reconciliation (12 resolved decisions, 6 feedback bodies, slice-DAG sanity passes). Apply two fixes, both leaning on primitives shipped in #2254: 1. Alive-signal gate at the per-agent alert sites. Before firing `heartbeat_timeout` or `progress_stall`, consult `PeerConsensusTracker.get_latest_progress_timestamp()` plus peer-heartbeat snapshots; defer if either has fired within `orchestrator_alert_progress_gate_seconds` (default 300s, 0 disables). Self-excluded so a solo silent agent still escalates. The escalated flag is intentionally not set on defer, so the next poll re-checks. 2. Phase-aware post-ACK confirm timeout. Plan phase now uses `orchestrator_plan_post_ack_confirmation_timeout_seconds` (default 300s); refine/implement keep the existing 180s default. Out of scope (call out in #2059 follow-ups): - "Anthropic completion in flight" SDK telemetry — the cleanest fix for the heartbeat detector, but requires SDK work; the alive-signal gate covers the common case at far lower cost. - Voluntary "I'm finalizing" heartbeats that reset the post-ACK timer — same SDK-side dependency. - Auto-attaching log-tail evidence to OVERSEER_ALERT messages. Same-role cross-phase pollution caveat documented in the existing `_check_brc_progress_gate` TODO applies here too: a phase-stamped heartbeat key would close both at once. * Fix checks: disable alive-signal gate in multi-agent stall tests The 4 failing tests in TestMultipleAgentsStalling create multiple agents where one or both are deliberately stalled. The new alive-signal gate (#2242) defers per-agent stall alerts when peer agents have heartbeats within orchestrator_alert_progress_gate_seconds (default 300s), which caused these tests to observe zero escalations instead of the expected per-agent escalations. Set orchestrator_alert_progress_gate_seconds=0 in the four failing tests so they isolate the per-agent escalation behavior from the peer-progress deferral, matching the pattern already used in test_health_monitor.py::test_heartbeat_timeout_per_agent. * Update file-size-allowlist baselines for peer_consensus.py and routes/pipelines.py Both files grew past their recorded baselines in the allowlist, causing the file-sizes custom check to fail on PR #2268. Update baselines to the actual current sizes so CI passes. - orchestrator/peer_consensus.py: 1988→2003 lines, 85268→85965 bytes - orchestrator/routes/pipelines.py: 15356→15514 lines, 669950→677112 bytes * Update file-size-allowlist baseline for select_tests.py PR #2262 grew scripts/select_tests.py from 1650 -> 1850 lines without updating the allowlist baseline (#2250 added the lint after PR #2262 was reviewed, so its CI did not catch the drift). Without this update, the merged result fails 'make lint-custom'. * Address review feedback: clarify BRC-bus self-deferral; filter peer heartbeats by active-agent set Reviewer flagged three actionable non-blocking concerns on #2268: 1. Docstring discrepancy. The PR description claims "self-excluded gate so a solo silent agent still escalates", but self-exclusion only applies to the peer-heartbeat path. ``get_latest_progress_timestamp`` aggregates proposals + matrix entries across the whole tracker, so on a single-producer pipeline the producer's own propose/ACK timestamp defers its own alert until ``gate_seconds`` elapses past that timestamp (effective stall window ≈ ``heartbeat_threshold + gate_seconds``). ``CONTAINER_STOPPED`` covers genuinely-dead containers; the delay only matters for hung processes inside live containers. Filtering by focal agent would require a new ``peer_consensus`` API — deferred. Docstring updated to call out the behavior explicitly. 2. Cross-phase heartbeat pollution. Reviewer noted the symmetry argument: ``_check_brc_progress_gate`` filters peer heartbeats by ``active_role_names`` to drop stale prior-phase heartbeats from the shared HealthMonitor; this gate did not. Added a snapshot of ``set(self._agents.keys())`` taken under the same lock as ``_last_heartbeat`` so prior-phase ghosts (agents whose containers were stopped without ``reset_agent``) cannot defer current-phase alerts. Same-role cross-phase pollution remains, tracked by the existing TODO. New regression test: ``test_gate_filters_inactive_agent_heartbeats``. 3. Misleading test override. ``test_check_brc_progress_uses_plan_phase _threshold`` set ``orchestrator_alert_progress_gate_seconds=0`` with a comment about isolating the phase-aware threshold path — but ``check_brc_progress`` doesn't consult ``_has_recent_peer_progress``, so the override implied a coupling that doesn't exist. Removed. Two other reviewer concerns left as-is: the sliced-pipeline tracker scope is acknowledged in the docstring (peer-heartbeat fallback covers it), and the file-size allowlist drift was absorbed by main and is now moot. * Active-role filter pulls from tracker graph, not self._agents Reviewer noted in 76c3aac that the active-agent filter in _has_recent_peer_progress is a no-op in production: every heartbeat write also populates _agents, so set(self._agents.keys()) is a static superset of _last_heartbeat.keys() and the filter never fires. Replace it with the tracker graph's all_roles() — the current-phase roster installed by concurrent_executor.spawn_active_phase_agents. The graph IS phase-scoped, so cross-phase ghosts in _last_heartbeat (which the singleton HealthMonitor doesn't reset on phase transition) are now actually dropped. When no tracker is registered (early startup, between phases, non-BRC phases) the filter is skipped — preserves the pre-#2242 peer-heartbeat fallback behavior. Update test_gate_filters_inactive_agent_heartbeats to register AGENT_ID_2 via _emit_heartbeat (production state shape) and mock a tracker whose graph excludes it, instead of injecting an impossible state into _last_heartbeat directly. * Fix docstring symbol name: spawn_active_phase_agents → spawn_all The reviewer flagged that concurrent_executor.spawn_active_phase_agents does not exist — the actual phase-spawn entry point is ConcurrentExecutor.spawn_all (concurrent_executor.py:349). Update both references in health_monitor._has_recent_peer_progress to point at the real method so docstring navigation lands at the right symbol. * Fix docstring symbol name: ConcurrentExecutor → ConcurrentPhaseExecutor The actual class in orchestrator/concurrent_executor.py is ConcurrentPhaseExecutor (line 113); ConcurrentExecutor does not exist outside test-file local aliases. Both docstring references in health_monitor.py now point at the real symbol so Sphinx :meth: resolves and code-navigation works. --------- Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
Three changes to
scripts/select_tests.pythat fix changeset-aware narrowing for the inner loop:Bare-name AST resolver — supplements grimp so source-code changes actually narrow. The codebase universally uses bare-name imports (406/407 tests, 33/33 sampled production files), but grimp registers production under fully-qualified names; without help it returns no downstream tests for almost every source change and the
no downstream tests for changed moduletrigger widens to the full suite. The resolver AST-scans every module, maps bare-name imports to their FQ production targets via the same prefix rules grimp'ssys.pathinjections imply, and unions the synthetic edges with grimp's reverse closure. End-to-end: editingorchestrator/action_guards.pynow selects 106/278 tests instead of widening to 278.Empty-diff short-circuit —
make teston a clean tree used to widen to the full suite via anempty difftrigger (minutes of pointless runs). Now the selector emits empty stdout +selected 0 tests (skipping pytest)and the Makefile skips pytest entirely. Unresolvable-baseline / stale-LKG still widen for safety.Drop the canary — every-10th-invocation full-suite tick was defense in depth against unknown static-analysis blind spots. With the AST resolver in place the analysis is much more thorough; remaining true blind spots (subprocess crossings, data-file loads, dynamic imports) are already covered by their own explicit triggers. Ground truth is
make test-allbefore push (which CI runs anyway).Verification
test_select_tests_bare_name.py).bundle.bare_name_upstreambuilds 197 keys / 1306 edges, and_walk_upstream_combinedreaches the expected test consumers for representative source modules.orchestrator/action_guards.pyproducedselect-tests: narrowed 106/278 tests in 3.29s (baseline=…, trigger=diff)and pytest ran the narrowed selection through to ~84% before I cancelled the timeout.select-tests: no changes since baseline …; selected 0 tests (skipping pytest), zero stdout.Related issues surfaced during this work
tests/llm/claude/test_runner.pyPython 3.14 /asyncio.get_event_loop()failure (pre-existing).tests/tree shadowed bysandbox/tests/during grimp graph build. Limits the resolver's reach: it can synthesise edges into existing graph nodes, buttests/<subdir>/test_*.pymodules are missing from the graph entirely. Filed as a separate fix.Test plan
make test-allpasses (the 80% coverage gate is unchanged — narrowing is local-inner-loop only).build_bare_name_indexagainst the prefix table (shared.,orchestrator.,sandbox.tools.,sandbox.)._walk_upstream_combinedis OK with grimp'sfind_downstream_modulessemantics (transitive closure of importers).