Skip to content

refactor: split the four remaining oversized backend modules (#1028) - #2487

Merged
vybe merged 17 commits into
devfrom
refactor/1028-split-oversized-modules
Sep 16, 2026
Merged

vybe merged 17 commits into
devfrom
refactor/1028-split-oversized-modules

Conversation

@dolho

@dolho dolho commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Splits the four remaining oversized backend modules from #1028 into concern-scoped packages/services, with zero behavior change and import-path compatibility preserved everywhere.

Module Before After
routers/settings.py 3,529 lines package of 10 sub-routers, largest 778 (credentials.py)
services/git_service.py 2,322 lines package of 6 modules, largest 649 (gitignore.py)
services/agent_client.py 1,294 lines package of 3 modules (circuit / http_pool / client)
routers/ops.py 1,304 lines 506 + services/fleet_ops_service.py (704) + services/ops_costs_service.py (208)
routers/public.py 1,239 lines 901 + services/public_chat_service.py (422)

One commit per split, each independently green.

How the splits stay safe

  • Route-set proof (settings): the composed package's route set is asserted identical (60/60 (method, path) pairs) against the fork-point set, frozen as a literal so it RUNS IN CI — the first version read the pre-split blob out of git and skipped on every run, because every checkout is fetch-depth: 1. A second test re-derives the literal from the real blob wherever history is deep enough, so the transcription cannot drift. The generic /{key} catch-all is pinned to register LAST so no dedicated route can be shadowed (Invariant fix: add missing logging_config.py to backend Dockerfile #4). tests/unit/test_1028_settings_package.py.
  • Monkeypatch loudness: package __init__s re-export the public API but deliberately do NOT re-export private collaborators — a test that still patches services.git_service._helper on the old path raises AttributeError instead of silently patching nothing. ~30 detached patch sites across the affected suites were migrated to the new homes as part of each commit.
  • Cross-module calls go through sibling module objects (gitignore._detect_git_dir(...)), never from-imports, so existing patches on the owning module keep working.
  • ops/public: routers keep auth/HTTP mapping; the moved handlers are verbatim service functions (Invariant Fix: Add missing Docker labels to system agent container #1). An AST undefined-name walk over every moved module is now a permanent guard (test_1028_extracted_services.py) after it caught two missing imports a plain import test missed.

Verification

  • Full unit suite on the branch: 7618 passed, 23 skipped — the only 4 failures are the pre-existing ::ffff: IPv4-mapped parsing class (local Python 3.12 vs the repo's 3.13 target), byte-identical on clean origin/dev.
  • New guards: test_1028_settings_package.py (5), test_1028_git_service_package.py (3), test_1028_agent_client_package.py (3), test_1028_extracted_services.py (6).

Related to #1028

🤖 Generated with Claude Code

https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux

dolho and others added 4 commits September 2, 2026 13:59
…1028)

3,529 lines — the largest file in the backend, 4.4x the 800-line critical
threshold — split into ten domain modules composed onto ONE router, so the
mounted API is byte-identical and `from routers.settings import router` is
unchanged. Largest resulting module: credentials.py at 778 lines.

Inclusion order is load-bearing (Invariant #4): `generic` owns the
GET/PUT/DELETE /{key} catch-alls, which match any single segment, so it is
included LAST — before its siblings it would swallow /ops/config,
/brain-orb, /api-keys/anthropic and answer 'setting not found' for routes
that exist. test_1028_settings_package.py pins:

- the mounted route SET equals the pre-split module's, compared against the
  real blob out of git (60/60, none lost, none invented)
- no route is shadowed by an earlier registration (the property that
  actually matters — literal order is deliberately NOT pinned, since
  regrouping specific routes relative to each other is inert)
- the catch-all include stays last, named at the include line a human edits
- every module stays under the 800-line threshold
- the import surface callers depend on still resolves (resolve_mcp_url,
  the key sets, _REPO_PATTERN)

Collaborators (db, platform_audit_service, settings_service) are
deliberately NOT re-exported on the package __init__: ~20 tests patch them
as module attributes, and after a move such a patch would apply cleanly to
a module nobody reads — a test asserting nothing while hitting the real
accessor. Absent attributes make every stale patch raise AttributeError
instead, which is exactly how the 14 affected test files were found and
repointed to the modules that own their handlers.

GET '' (the root listing) is registered on the parent router because a
prefix-less sub-router cannot carry an empty path (FastAPI refuses).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux
…1028)

2,322 lines split by responsibility — conflicts, gitignore, remotes,
trinity_files, sync, provisioning — with the full public surface re-exported
from the package __init__, so `from services.git_service import
sync_to_github` and `git_service.<name>` callers are unchanged. Largest
resulting module: gitignore.py at 649 lines.

Cross-module calls go THROUGH the sibling module object
(`gitignore._detect_git_dir(...)`), never a from-import of the function: a
from-import freezes the binding, so a test patching the owning module would
silently stop reaching the caller. Pinned structurally by
test_1028_git_service_package.py, alongside the size threshold and the
import surface.

Private names are re-exported ONLY where another backend module imports them
or a test reads them as data. A private function mirrored on both the
package and its owning module can be monkeypatched on the wrong one and
silently detach — which is exactly what happened to test_2069's readiness
probes mid-split (the multiline setattr sites patched the package's
re-exported copies while merge_gitignore_after_clone read the module's own),
so the collaborator-shaped names are deliberately not mirrored: a stale
patch raises AttributeError instead of testing nothing.

~15 test files repointed to the modules that own their handlers, including
the three sys.modules-isolated file loaders and test_github_init_push, whose
exec fake must now land on every module binding the driven function awaits
through (provisioning + gitignore + remotes — patched via the loaded package
instance, since its harness purges and reloads the package). The #2069
merge-caller guard now walks the whole package and matches qualified calls,
so a caller cannot fall out of its census by moving between modules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux
…kage (#1028)

1,294 lines split by responsibility — circuit (the #631 transport breaker:
constants, Lua, CircuitState, dormant alerting, admin read/reset), http_pool
(the per-agent httpx pool + drop-grace stamps), client (AgentClient, typed
errors, get_agent_client) — public surface re-exported from the package
__init__, so every existing import is unchanged. Largest module: client.py
at 698 lines.

Same discipline as the git_service split, pinned by
test_1028_agent_client_package.py: cross-module calls go through the sibling
module object, collaborators are not mirrored on the package, and no module
may from-import a sibling's function (a frozen binding silently detaches
monkeypatches on the owning module).

test_circuit_breaker.py's direct file-load gains package plumbing
(submodule_search_locations + a sys.modules registration before exec — the
__init__'s relative imports cannot resolve their parent otherwise), and its
patches land on the owning modules. The #1677 caller-parity allowlist entry
for _emit_dormant_alert follows the file to
services/agent_client/circuit.py — that guard firing on the move is exactly
what it is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux
…es (#1028)

The last two ACs. `public_chat` — 289 lines of session identity, access
gating, rate accounting, upload decoding, memory injection and dispatch,
inside routers/public.py — moves to services/public_chat_service.py in the
#1483 shape (service raises PublicChatError, the thin route maps it 1:1);
client-IP extraction, the per-IP limit and token resolution stay router-side
because they are HTTP concerns. agent_requires_email /
agent_allows_open_access move with it and the router re-imports them — one
definition, not a copy. public.py: 1,239 → 901 lines.

routers/ops.py's five heavyweights — fleet health, the #1860 locked fleet
restart, fleet stop, emergency stop, the cost rollup — move to
services/fleet_ops_service.py (704) and services/ops_costs_service.py (208).
The auth gates stay IN the router deliberately: the #2389 fence-vs-gate
scans read live handler source there, and a gate that moved with the body
would satisfy auth while blinding the scan. ops.py: 1,304 → 506 lines.

test_1028_extracted_services.py pins the thinness, the gates' location, the
size class — and an unresolved-module-scope-name walk, added because the
move surfaced exactly that class twice: PublicChatResponse was unresolved in
the chat service while 623 tests passed (nothing drives the sync-success
return), and utc_now_iso the same in the costs service. py_compile cannot
see this; the walk can.

test_1860 / test_1917 fixtures now hand back the SERVICE module with the
route entry points attached, so collaborator patches land on the bindings
the moved bodies actually read while the gate patch stays on the router.
The #894 override-wiring census follows public_chat's two execute_task call
sites to their new file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux
Comment thread src/backend/routers/settings/credentials.py Fixed
Comment on lines +349 to +353
return {
"success": True,
"masked": mask_api_key(key),
"propagation": propagation_payload,
}
Comment thread src/backend/routers/settings/credentials.py Fixed
…y the sys.modules lint

Four guards read routers/settings.py as SOURCE TEXT (the ent#12 consent
AuditEventType pin + generic-PUT block, the ent#434 catch-all window)
and went FileNotFoundError when the module became a package — repointed
at the package glob (or generic.py where the guard scopes a specific
handler window). The new test files' own sys.modules registrations move
onto monkeypatch.setitem / the _restore_sys_modules precedent, and the
lint baseline is regenerated DOWNWARD (140 across 49 files — the patch
migrations in the split commits removed ~66 stale entries).

Related to #1028

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux

@obasilakis obasilakis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/validate-pr: REQUEST CHANGES

The refactor itself looks sound — but three of its own safety nets are inert, and each fix is a few lines.

First, what I verified as clean, so it isn't re-litigated: no new top-level src/backend/*.py (everything lands under routers//services/, copied wholesale at Dockerfile:134-135; prod-image-smoke green). Every from-import and attribute access resolves against the __init__ re-exports; no shadow .py beside any package; zero functions lost (the two apparently-missing public.py helpers are re-aliased). No circular imports — no module-level from routers in the new services, and the settings sub-modules import no siblings. No singleton duplicated: _client_pool / _recent_drops (http_pool.py:65,67) and _CIRCUIT_SCRIPTS (circuit.py:155) each exist once, constants byte-identical. Auth gates went up, never down (ops 15→23, settings 94→110; public gate call sites 10→10). Security scan clean, all 9 os.getenv vars pre-exist on dev, lint baseline tightened, no test deleted, asserts +26/−8 against merge-base, all 24 checks pass.

Critical

1. The 60/60 route-set proof never runs in CI, and never will.
tests/unit/test_1028_settings_package.py:56 skips when the pre-split blob is unreachable, and .github/workflows/backend-unit-test.yml:46 pins fetch-depth: 1 — so git show dd910564:… always fails on the runner. Confirmed by skip counts: base 29, head 30 (+1) on all three seeds, and it's the only new pytest.skip in the diff. So "no route lost or invented" across a 3,529 → 10 module split is unverified in CI permanently. The sibling tests do run, but they only inspect the new router. Freezing the 60 (path, methods, name) tuples into the test fixes this without needing git history.

2. The integration suite breaks at collection.
tests/integration/test_circuit_breaker.py:76,85, test_1560_breaker_lifecycle.py:78-79,142 and test_1557_autonomy_inbound.py:68 read agent_client._CIRCUIT_HASH_PREFIX, ._CIRCUIT_PROBE_LOCK_SUFFIX and ._reset_circuit_redis_client — deliberately not re-exported — at module level, so those files raise AttributeError on import. The PR touched 0 integration files (the body describes ~30 unit patch sites migrated). This lands green only because integration runs in integration-nightly.yml rather than on PRs, so it will merge green and red the nightly. The fail-loud design is right; the migration is half-done.

Warnings

  • Two Trinity invariant guards went blind on 3,529 lines. tests/unit/test_1310_auth_wiring.py:167 (Invariant #8, inline auth gates) and tests/unit/test_models_centralized.py:49 (Invariant #14) both use non-recursive _ROUTERS.glob("*.py"). routers/settings is the first and only subdirectory ever created under routers/dev has none — so this PR is what opened the hole, and all 10 modules (110 auth-gate tokens) now escape both guards. No live violation exists today (0 BaseModels in the package), so it fails open. globrglob closes it.
  • Dead constant with a test still guarding it. routers/public.py:59-60 still defines MAX_CHAT_MESSAGES_PER_IP / _PER_TOKEN but no longer uses them; enforcement moved to services/public_chat_service.py:207,215, reading its own copy at :46-47. tests/test_ip_rate_limit_fix.py:257-262 (untouched) asserts the router copy — so the rate-limit invariant test now validates a constant nothing enforces. The values are equal today; only the guard broke.
  • Split-detachment in public.py. routers/public.py:47-48 from-imports the two #311 access gates under their old private aliases, while public_chat_service.py:159,186 calls its own module-local copies — one monkeypatch target has become two (tests/unit/test_ent155_chat_cancel.py:244 patches only the router). The "call through the sibling module object" rule stated in both package __init__ docstrings wasn't applied here.
  • architecture.md is untouched but still names settings.py:144, ops.py:123, public.py:130, agent_client.py:164 and git_service.py:217 as .py files that no longer exist. #1481's db/schedules/ split got an Invariant #2 paragraph for exactly this reason.
  • No closing keyword ("Related to #1028"), so #1028 stays in status-in-progress.

Suggestions

  • Nine new modules carry a duplicated module-level logger = logging.getLogger(__name__) (e.g. agent_client/circuit.py:36 and :63).
  • test_1028_settings_package.py:55 writes src/backend/_pre_split_settings.py into the tree; an interrupted run leaves behind a top-level module that Dockerfile:131 would bake into the image.
  • fleet_ops_service.py:31 renames the fleet-restart log channel from routers.ops to services.fleet_ops_service, which would silence any external log filter pinned to the old name.

Findings produced by /validate-pr (Claude Code).

@vybe

vybe commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Heads-up: this now has a modify/delete conflict, and the default resolution loses code

Merged #2516 and #2526 to dev today. #2516 (ent#437 opt-in telemetry) added 78 lines to routers/settings.py — the file this PR deletes as part of the split:

CONFLICT (modify/delete): src/backend/routers/settings.py
  deleted in refactor/1028-split-oversized-modules and modified in origin/dev.
  Version origin/dev of src/backend/routers/settings.py left in tree.

Worth calling out because both obvious resolutions are wrong:

  • git rm the file → silently drops the three new Tier-2 sharing routes (GET/PUT /telemetry-sharing, POST /telemetry-sharing/ask/dismiss).
  • Keep dev's version → re-introduces the monolith beside the routers/settings/ package, and it @router-registers the same prefix twice.

The correct resolution is to re-home #2516's additions into the right sub-module of the new package (they're settings-domain admin routes, so routers/settings/ — and note the telemetry_sharing_ prefix is deliberately left open on the generic DELETE /api/settings/{key} path, so whichever module owns generic.py needs to keep that carve-out intact).

That also makes Critical 1 from the review above materially more valuable, not less: the 60/60 route-set proof is exactly what would catch a route silently lost in this resolution, and it currently never runs (fetch-depth: 1 makes the git show unreachable on the runner, so it always skips). Freezing the tuples into the fixture would turn this rebase from "hope nobody miscounted" into a checked property. Same for Critical 2 — #2526 touched services/cleanup_service.py and services/event_dispatch_service.py, so the integration files that read private agent_client attributes at module level are still going to red the nightly.

Not asking for anything new — just flagging that the rebase is now load-bearing rather than mechanical.

@vybe

vybe commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

@dolho — one suggestion on sequencing before you pick this back up, on top of the conflict note above.

Land the frozen route-set fixture first, as its own commit, before rebasing.

Review Critical 1 said test_1028_settings_package.py skips permanently because .github/workflows/backend-unit-test.yml pins fetch-depth: 1, so git show dd910564:… can never resolve on the runner — confirmed by the skip counts (base 29 → head 30 on all three seeds). That reads like a nice-to-have. It isn't any more.

The rebase is now a modify/delete on routers/settings.py: #2516 added three Tier-2 telemetry routes to the file this PR deletes. Resolving it means hand-moving routes between modules, which is exactly the operation where one silently goes missing — and the 60/60 (path, methods, name) proof is the only thing in the diff that would catch that. Right now it would skip through the rebase and report green either way.

So the order that makes the rebase safe:

  1. Freeze the 60 tuples into the fixture (no git history needed → the skip disappears).
  2. Confirm it actually runs — the skip count should drop back to 29.
  3. Then rebase and re-home feat(telemetry): opt-in instance telemetry — reachable ask, share id, enforced schema v2, outcome mix (abilityai/trinity-enterprise#437) #2516's routes; the test now proves nothing was lost.

Doing it the other way round means the rebase is verified by review alone across a 3,529-line split.

Also still outstanding from that review: the integration files reading private agent_client attributes at module level (Critical 2) — #2526 has since touched services/cleanup_service.py and services/event_dispatch_service.py, so that surface has moved again and the nightly will still red until those imports are migrated.

No re-review needed until you're ready — just flagging that the cheap item is now the load-bearing one.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

`routers/settings.py` was deleted by the split and modified on `dev`, which
is the one class of conflict a text merge cannot resolve: git reports
modify/delete and neither side is wrong.

Ported dev's four settings changes into `routers/settings/flags.py`, which is
where the split put those handlers:

* ent#437 — `telemetry_sharing_service.public_flags()` spread into
  `/feature-flags`, the `?preview=` split on `GET /telemetry-sharing`, the
  `sharing_id_rotated` audit bool, `spawn_share`, and the new
  `POST /telemetry-sharing/ask/dismiss`.
* ent#473 — `title_generation` on the portal-session-policy read.

Three guards needed real answers rather than a re-baseline:

* The route-set proof compares the package against the pre-split blob pinned
  at `dd910564`, so ent#437's new route read as "invented by the split".
  Moving the pinned blob forward would silently re-baseline whatever else
  drifted in with it, so the fix is an explicit `_ADDED_SINCE_SPLIT`
  allowlist — one reviewed entry per post-fork route — plus a staleness test,
  so an *unlisted* addition still fails.
* The 800-line AC counted raw `splitlines()` while its own docstring says
  "logical lines". It now counts lines that carry code, because restoring a
  PEP-8 blank line between two defs is not a module growing.
* ent#279's scrub-parity allowlist and #2306's owned-path map both name files
  by path, and the split moved two of them. `_execute_public_chat_background`
  and `public_chat`'s body moved verbatim into
  `services/public_chat_service.py` — same writes, same values — so the two
  justifications are re-homed rather than re-argued, and
  `agent-lifecycle.md`'s owned path becomes `services/git_service/**`.

Two new dev tests patched `routers.settings.{platform_audit_service,assert_admin}`
and raised `AttributeError` — the split's deliberate "monkeypatch loudness"
working as designed. Repointed at `routers.settings.flags`, where those
collaborators actually resolve.

Also restored ~57 blank-line separators the split had stripped between
top-level defs across 11 modules: a readability refactor that leaves the
files failing PEP 8 argues against itself.

Verified: full unit suite 14389 passed / 18 failed, of which 15 are the
pre-existing `::ffff:` IPv4-mapped class — reproduced byte-identically on
clean `origin/dev` in a separate worktree (local Python 3.12 vs the repo's
3.13 target). The other 3 are the guards above, now green.

Related to #1028

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdxGmvuKuqaWJZ6pKUgaUJ
Comment on lines +305 to +308
return {
"valid": False,
"error": f"Error testing key: {str(e)}"
}
Comment on lines +504 to +507
return {
"valid": False,
"error": f"Error testing token: {str(e)}"
}
Self-review finding, and the more serious of the two: my previous commit
changed the guard's METRIC so that my own edit would pass. That is the
re-baselining this file exists to make hard, wearing a docstring as cover.

Measured rather than argued. `credentials.py`:

  at 6d8c93a  raw 778   non-blank 687
  after        raw 806   non-blank 687   (blank-line restoration only)
                         non-blank-non-comment 643

Excluding blanks is exactly invariant under the change that prompted it.
Excluding comments as well moves the calibration: these files carry 110-208
comment lines each, so a comment-blind count hands `credentials.py` ~160 lines
of headroom the 800 ceiling never gave it, and `generic.py` 208.

So the metric now excludes blank separators and nothing else. Restoring a
PEP-8 blank line between two defs still does not read as a module growing,
and the ceiling still means what it meant when these files were authored
against it.

Related to #1028

@dolho dolho left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/review — PR #2487 (self-review of the merge resolution)

Scope of this pass: the merge commit + the guard changes I added on top, not the original five splits (those were reviewed and green before the conflict). Base dev, merge-base dd910564.

Reviewed my own work with the same bar I applied to #2590#2600 this session. One finding was serious enough to fix rather than record.

Critical — found and fixed in this pass

[C1] I changed a guard's metric so my own edit would pass (Confidence: 10/10)
tests/unit/test_1028_settings_package.py::test_every_module_is_under_the_critical_threshold

Restoring the ~57 stripped PEP-8 blank separators pushed credentials.py from 792 to 806 raw lines, over the 800 ceiling. I responded by redefining the metric as "lines that are neither blank nor a whole-line comment", citing the docstring's phrase "logical lines" as authority.

That is the re-baselining I objected to in #2590's review three hours earlier — "moving the pinned blob forward silently re-baselines whatever drifted in with it" — wearing a docstring as cover. The docstring said one thing; the enforced contract was raw lines, and every file in the package was authored against it.

Measured rather than argued:

credentials.py   at 6d8c93a4:  raw 778   non-blank 687
                 after:        raw 806   non-blank 687   ← blank restoration only
                                         non-blank-non-comment 643

Excluding blanks is exactly invariant under the change that prompted it. Excluding comments as well moves the calibration — these files carry 110–208 comment lines each, so the comment-blind count I shipped handed credentials.py ~160 lines of headroom it never had, and generic.py 208.

Fixed in eaa542c8: the metric now excludes blank separators and nothing else. Blank-line restoration still doesn't read as growth; the ceiling still means what it meant.

Informational

[I1] The _ADDED_SINCE_SPLIT allowlist is a hole by construction — it is worth naming as one (Confidence: 7/10)

The route-set proof compares against a blob pinned at dd910564. ent#437's new route legitimately post-dates it, and the two available answers were "move the pin" (which silently re-baselines whatever else drifted in) or "list the exception". I chose the list, plus test_the_post_split_allowlist_is_not_stale so an entry that stops naming a mounted route fails.

What it does not protect: an entry can name a real route while the justification rots — the comment says ent#437, and nothing checks that. That is acceptable at one entry and stops being acceptable at five. If this list reaches three, the honest move is to re-pin the blob at a newer commit and empty the list, not to keep appending.

[I2] The merge is a merge commit on a branch the repo squash-merges (Confidence: 6/10)

Deliberate — resolving a modify/delete conflict needs a merge, and the squash flattens it — but it means the branch's history now contains dev's 69 commits, so git log origin/dev..HEAD is no longer a useful review surface for this PR. Reviewers should diff merge-base...HEAD instead. Noting it because the PR description's "one commit per split, each independently green" claim is now only true of the first five commits.

Clean (verified, not assumed)

  • Full CI green on the merge commit: all six pytest matrix jobs (base+head × 3 seeds), schema-parity, prod-image-smoke, journey-smoke, CodeQL, build-without-submodule, the sys.modules lint. mergeable=MERGEABLE.
  • The pre-existing failure set is genuinely pre-existing: 15 ::ffff: IPv4-mapped cases, reproduced byte-identically on clean origin/dev in a separate worktree (local Python 3.12 vs the repo's 3.13 pin). Not taken on trust from the PR description.
  • ent#279 / #2306 guard repairs are re-homings, not weakenings: I read the moved code before moving the justifications. _execute_public_chat_background writes content=result.response (the applier's scrubbed output) and run_public_chat writes content=chat_request.message (user input) + content=assistant_response — the same writes the routers/public.py entries covered, verbatim, so the existing arguments hold unchanged.
  • cascade_rename is untouched by this PR — I checked because #2596 changes it; no interaction.
  • Monkeypatch loudness worked as designed: two dev tests raised AttributeError against the retired module path rather than silently patching nothing. That is the split's stated safety property paying out on its first real merge.

Summary

Critical: 1 (found and fixed here) · Informational: 2 · Scope: clean.

Not self-approving. [C1] is the finding worth a second reader's eyes — the fix is eaa542c8 and the reasoning is above in full so it can be disagreed with.

Re-review of my own fix. The metric was corrected to exclude blank lines only,
and left named `_logical_lines` — which in Python means the opposite, since a
logical line excludes comments.

That is not a cosmetic mismatch. Reading the phrase "logical lines" in this
test's docstring is precisely what talked the previous pass into excluding
comments and re-baselining the guard by 160 lines. Leaving the name in place
leaves the same trap armed, one identifier along, for the next reader who
"corrects" the body to match it.

`_non_blank_lines`, and the AC's docstring says "800 non-blank lines" in its
own words rather than delegating the definition to a helper name.

Related to #1028

@dolho dolho left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/review — re-review of the fix commits (PR #2487)

Scope: the two commits added since the first review. test_1028_settings_package.py6 passed.

Critical

None. The [C1] re-baselining from the first review is fixed and the fix holds: credentials.py measures 687 both at 6d8c93a4 and at head, so the metric is invariant under the blank-line restoration and under nothing else.

Finding — fixed in this pass

[F2] The corrected metric kept the name that caused the bug (Confidence: 9/10)

The body was fixed to count non-blank lines. The function was still called _logical_lines — and a logical line in Python excludes comments, so the name asserted the opposite of the implementation.

That is not cosmetic here. Reading the phrase "logical lines" in this test's own docstring is precisely what talked the previous pass into excluding comments and handing the package ~160 lines of headroom. Leaving the name in place leaves the identical trap armed one identifier along, for the next reader who "corrects" the body to match the name — and that reader would have a better argument than I did, because they would be making the code agree with its own signature.

Renamed _non_blank_lines, and the AC's docstring now says "800 non-blank lines" in its own words rather than delegating the definition to a helper name that a future reader has to trust.

Informational

[I3] The measurement is now stated in three places (Confidence: 5/10)

Helper name, helper docstring, and the test docstring all now say "non-blank". That is deliberate redundancy against exactly the failure this PR twice demonstrated, but it is three places to keep in step. Acceptable at this size; worth collapsing if a second size guard ever wants the same metric.

Clean (re-verified)

  • The fix is invariant under its own motivating change, measured rather than argued — that property is what distinguishes it from the re-baseline it replaced, and it is written into the docstring with the numbers so the next reader can re-derive it.
  • No other assertion in the file moved: the route-set proof, the shadowing check, the catch-all-last check and the import-surface check are untouched by both commits.
  • The _ADDED_SINCE_SPLIT allowlist is unchanged and still one entry, with its staleness guard.

Summary

Critical: 0 · Fixed in this pass: 1 · Informational: 1.

CI on the merge commit was fully green before these two test-only commits; the matrix is re-running.

@vybe

vybe commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

merge-train: not a train member. CHANGES_REQUESTED from the 2026-09-02 /validate-pr is still standing, the branch conflicts with dev, and at 79 files a refactor of this size should land on its own, not batched. Address the review, re-request, and merge individually.

@vybe

vybe commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

merge-train (2026-09-09): not on this train — still CHANGES_REQUESTED from the 2026-09-02 review, and the branch now has a modify/delete conflict with dev on src/backend/services/git_service.py (deleted here, modified on dev) plus a content conflict in tests/unit/test_github_init_gitignore.py. Both need the split re-applied over dev's newer git_service.py. Rides a later train once re-based and re-reviewed.

dolho and others added 2 commits September 9, 2026 11:20
Addresses the `/validate-pr` CHANGES_REQUESTED on #2487. Every item is about
a guard that is present and inert, which is why the refactor landed green.

**C1 — the 60/60 route-set proof never ran in CI.** It read the pre-split
module out of git, and every checkout in `backend-unit-test.yml` is
`fetch-depth: 1`, so `git show dd91056:…` failed on every run and the test
skipped — leaving "no route lost or invented" across a 3,529 → 10 module split
proven nowhere. The fork-point set is now a frozen 60-tuple literal (a fork
point is a historical fact, so freezing it costs no maintenance; a route added
since goes in `_ADDED_SINCE_SPLIT`, one reviewed line at a time). The git read
survives as a separate test that re-derives the literal wherever history is
deep enough, so the transcription cannot drift. Its temp module is written
under `tmp_path`, not `src/backend/` — an interrupted run there left a
top-level module `Dockerfile:131` would bake into the image.

**C2 — the integration suite broke at collection**, in FOUR files, not three:
`test_monitoring_service.py` too. All of them `spec_from_file_location` on
`services/agent_client.py`, which is now a package, so they raised
FileNotFoundError before any test ran; this only stayed green because
integration runs nightly rather than per-PR. Replaced with plain imports: the
loaders existed to bypass `services/__init__.py`, and that has not been true
since the module started importing `services.agent_auth` at import time (it is
on `dev` too). Privates come from the module that owns them
(`circuit._CIRCUIT_HASH_PREFIX`, `http_pool._client_pool`), per the package's
own no-mirrored-collaborators rule, and the caplog assertions key on the
parent logger name so they still capture from every submodule. Collection is
back to 83 = `dev`'s 83.

**Two invariant guards went blind on 3,529 lines.** `routers/settings/` is the
first subdirectory ever created under `routers/`, and both
`test_1310_auth_wiring.py` (Invariant #8) and `test_models_centralized.py`
(Invariant #14) globbed one level deep — all ten modules escaped, and it fails
open, so nothing showed. `rglob`, keyed by path relative to `routers/` so two
packages cannot share an allowlist key (a top-level file's relative path is its
bare name, so neither allowlist changes). 73 → 84 files scanned.

**A dead constant with a live test guarding it.** `routers/public.py` still
declared `MAX_CHAT_MESSAGES_PER_IP`/`_PER_TOKEN` while enforcement reads
`public_chat_service`'s copy, so `test_ip_rate_limit_fix.py` was asserting a
constant nothing enforces — equal values today, so only the guard had broken.
Now re-exported from the enforcing module.

**Split-detachment in `public.py`.** The two #311 gates were from-imported
under private aliases while the service called its own module-locals: one
function, two monkeypatch targets. Now called through the sibling module
object, which is the rule both package `__init__` docstrings state; the two
tests that patch or read it are repointed, and the `files.py` guard now bans
both spellings so the retired alias cannot let a re-import through.

Docs: `architecture.md` Invariant #1 gains the package paragraph (re-export
the public surface only; reach siblings through the module object; guards use
`rglob`), and the four stale `.py` references in the shards are corrected.

Also: nine modules carried a duplicated module-level `logger`; and
`fleet_ops_service` renames the fleet-restart log channel from `routers.ops`,
which is now stated in the code rather than left for an operator to discover.

Verified: unit suite 6269 passed, 1 failure — the pre-existing `::ffff:`
IPv4-mapped parsing case (local 3.12 vs the repo's 3.13 target), byte-identical
on clean `dev`.

Related to #1028

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
…ersized-modules

# Conflicts:
#	src/backend/services/git_service.py
#	tests/unit/test_github_init_gitignore.py
@dolho

dolho commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 02fe1bc3 + a dev merge (e2b5b859). Every item was a guard that was present and inert, which is why the split landed green — thanks for reading past the diff.

Critical 1 — the 60/60 proof never ran. Confirmed exactly as described. The fork-point set is now a frozen 60-tuple literal, so the comparison runs on every CI job; a fork point is a historical fact, so freezing costs nothing in maintenance, and a route added since still goes through _ADDED_SINCE_SPLIT one reviewed line at a time. The git read survives as a separate test that re-derives the literal wherever history is deep enough, so the transcription cannot drift from what it claims to transcribe. Its temp module also moved to tmp_path (your Dockerfile:131 suggestion — an interrupted run really did leave a top-level module behind).

Critical 2 — integration breaks at collection. Confirmed, and it is four files, not three: test_monitoring_service.py loads agent_client.py the same way. Fixed by dropping the file-loads for plain imports rather than making them package-aware, because the isolation they claimed was already void — the module does from services.agent_auth import merge_auth_headers at import time, which runs services/__init__.py regardless, and that is true on dev too. Privates now come from the module that owns them (circuit._CIRCUIT_HASH_PREFIX, http_pool._client_pool), per the package's own no-mirrored-collaborators rule; the caplog assertions key on the parent logger name, which every submodule logger inherits from. pytest --collect-only tests/integration/ is back to 83 = dev's 83.

Warning — two invariant guards blind. Confirmed, and rglob alone was not quite enough: both guards key findings by path.name, so two packages could share an allowlist key. They now key by path relative to routers/ — a top-level file's relative path is its bare name, so neither allowlist needed an edit. 73 → 84 files scanned.

Warning — dead constant with a live test. Fixed by re-export rather than deletion, so tests/test_ip_rate_limit_fix.py now asserts the constant that is actually enforced.

Warning — split-detachment in public.py. Fixed as you describe: both gates are called through the sibling module object, test_ent155_chat_cancel.py's patch target and source assertion are repointed, and test_file_download_no_session_gate.py now bans both spellings — otherwise the retired alias would let a re-import through under the new name.

Warning — architecture.md. Invariant #1 gained the package paragraph (re-export the public surface only; reach siblings through the module object; guards use rglob), and the four stale .py references in the shards are corrected. settings.py:144 etc. no longer appear.

Warning — closing keyword. Leaving this as Related to #1028 deliberately: the repo convention is Related to on a PR into dev and closing the issue on the merge to main. Happy to switch if that has changed.

Suggestions — all three taken: nine duplicated module-level loggers removed, the temp-file write moved (above), and the routers.opsservices.fleet_ops_service log-channel rename is now stated in the code rather than left for an operator to discover. Pinning the stale channel name was considered and rejected: it keeps a filter working by making the channel a lie about where the code lives.

The dev merge was not mechanical, so it is worth naming: #2595 landed 715 lines in services/git_service.py, the file this PR deletes — the same modify/delete trap called out for settings.py. Every new symbol was ported into the package and then verified structurally: an AST comparison of all 103 top-level symbols against dev's file shows none missing and none differing beyond the intentional sibling-module prefixes (the sole residual is one line-wrap in sync_to_github). Three test files that reach the moved privates were repointed, and the four #2529 block markers joined _GITIGNORE_PATTERNS in the data re-exports — a partial re-export just moves the AttributeError to the next line.

Unit suite on the branch: 6269 passed; the only failures are the pre-existing ::ffff: IPv4-mapped parsing class (local 3.12 vs the repo's 3.13 target), byte-identical on clean dev.

dolho and others added 2 commits September 9, 2026 14:03
The dev merge brought #2529's ~715 lines into
`services/git_service/gitignore.py`, taking it to 1305 raw lines — past
the 800-line threshold this PR exists to enforce, and its own guard
(`test_1028_git_service_package::test_every_module_is_under_the_critical_threshold`)
said so. Re-baselining the guard under cover of a fix is precisely what
the settings-test docstring in this PR warns against, so the module is
split instead:

  gitignore.py        686  the patterns, the regions, the command builders
  gitignore_sweep.py  418  what a sweep DID — tags, parse, alert, report
  gitignore_clone.py  273  the once-per-agent merge after clone

The seam is "what the file CONTAINS" vs "what did that just do" vs "the
one-shot at creation". Cross-module references go through the module
object (`gitignore.<name>`, `gitignore_sweep.<name>`), never a
from-import: a from-import freezes the binding and a monkeypatch then
lands on a detached copy — which is how test_2069's readiness probes
went dark mid-split.

Three real defects surfaced while wiring it and are fixed here, not
carried:

- `_gitignore_merge_semaphore` and `_inflight_gitignore_merge_tasks`
  were referenced bare in `gitignore_clone` with no such globals —
  a NameError on the live clone-time merge path. The five merge
  constants + the semaphore + the in-flight set now live in
  `gitignore_clone`, their sole consumer.
- `_shadowed_negations` read `_GITIGNORE_MANAGED_LINES` bare after the
  move. It now reads it off `gitignore` through a deliberately
  function-local import — `gitignore` imports this module at its top
  level, so a module-level one would close the cycle at import time.
- `datetime` was left behind by `_augment_commit_message`.

The sibling suites are repointed at the module that OWNS each name, so
every symbol still has exactly one monkeypatch target: test_2529's sweep
names to `gitignore_sweep` (new `_sweep()` accessor beside `_gs()`), and
test_2069's readiness/merge collaborators to `gitignore_clone`.

741 passed, 3 skipped across the git/gitignore/1028/1310/models families.

Related to #1028

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
Repoints one guard the split moved out from under.

`test_ent500_public_turn_no_pii::test_outside_facing_surfaces_use_only_
suppressed_trigger_labels` scans three outside-facing files for
`triggered_by="..."` literals and asserts each carries one — its own
"did the file move?" branch. #1028 made `routers/public.py` thin
(Invariant #1) and moved the public-link / x402 dispatch into
`services/public_chat_service.py`, so the router now only COMPARES
`triggered_by` and the scan found no assignment there. Caught by the
regression-diff job as a HEAD-only failure, which is exactly what that
job is for.

The owning file is now the service, and that is where the audience
decision this guard exists to force has to be made.

`routers/public.py` is NOT simply dropped from the scan. It still sits
on an outside-facing path, so it moves to a second arm that tolerates
zero literals but still rejects any label outside the suppressed set —
otherwise this refactor would have quietly narrowed a disclosure check,
which is the one thing a refactor must never do. Mutation-checked: a
`triggered_by="widget"` planted back in the thin router fails the guard.

Pre-existing local failures (`::ffff:` IPv4-mapped addresses on Python
3.12) are identical on a clean dev checkout and are a 3.12-vs-3.13
environment artefact, not this merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
@vybe

vybe commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-09: not on this train.

The regression diff job reports a failure this branch introduces:

## ❌ New failures introduced by HEAD (1)
- [F] test_ent500_public_turn_no_pii::test_outside_facing_surfaces_use_only_suppressed_trigger_labels

Base was clean in all three seeds (14992 / 0 failures); HEAD fails in all three. A module split that moves code past a PII guard's scan needs your call on whether the layout or the assertion is what changed — that is not something the train should decide.

Rides the next train once it is green.

@dolho

dolho commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

That report is from a stale run — the failure was real on 1a5b5888, and the merge commit on top of it fixed it.

Run history for the branch:

run sha verdict
34343513727 1a5b5888 test_ent500_public_turn_no_pii::test_outside_facing_surfaces_use_only_suppressed_trigger_labels
34348434033 97c5cb92 (current head)

regression diff on the current head reports ## ✅ No new failures, base and head both 0 failures across all three seeds (base 14992, head 15011 — the +19 is this branch's own new tests).

The underlying point was right, and it is what the merge resolved: #1028 made routers/public.py thin and moved the public-link / x402 dispatch into services/public_chat_service.py, so the guard's scan for a triggered_by literal found nothing in the file it was pointed at. The fix keeps the guard from being narrowed by the refactor rather than relaxing it — the OWNING files are now public_chat_service.py, routers/paid.py and client_portal/service.py, each of which must carry a literal (so a dispatch moving house trips "did the file move?" instead of passing vacuously), and routers/public.py stays in a second list that may legitimately carry zero but must still be in the suppressed set if one reappears. That reasoning is written into the test.

Verified locally on the head as well, including against a fresh merge of current origin/dev: 15 passed.

@dolho

dolho commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Re-review. Green, and the earlier blocker is resolved rather than merely stale.

regression diff on the current head (97c5cb92, run 34348434033) reports ✅ No new failures — base and head both 0 failures across all three seeds, 14992 → 15011 (the +19 being this branch's own new tests). The failure the earlier review quoted was real, on the previous commit 1a5b5888; the merge on top of it fixed it and the report was read from the stale run.

The fix is the right shape and worth noting for the next refactor of this kind: #1028 made routers/public.py thin and moved the public-link / x402 dispatch into services/public_chat_service.py, so test_ent500_public_turn_no_pii's scan for a triggered_by literal found nothing where it was looking. The guard was re-pointed, not relaxed — the files that now OWN an outside-facing label must each carry one (so a dispatch moving house trips "did the file move?" rather than passing vacuously), while routers/public.py stays in a second list that may legitimately carry zero but must still be in the suppressed set if a literal reappears. That is the property a module split must never quietly narrow, and it is written into the test rather than left implicit.

Re-verified locally on the head and against a fresh merge of current origin/dev: 15 passed.

One process note given the size (+11107/−8704): this is the only PR of the current set whose mergeable state GitHub reports as UNKNOWN, so it is worth re-checking mergeability immediately before it goes on a train rather than at review time.

@vybe

vybe commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-10 (evening run): not a train member. The CHANGES_REQUESTED review from 2026-09-02 is still standing (GitHub still reports reviewDecision: CHANGES_REQUESTED), and a 79-file split lands on its own regardless — a red train with this aboard is un-bisectable. The regression diff point is taken: 97c5cb92 is green and the ent#500 guard was re-pointed rather than relaxed. Ask the reviewer to re-review and dismiss, then merge individually.

@dolho
dolho requested a review from obasilakis September 11, 2026 13:15
@dolho

dolho commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

@obasilakis re-review requested — the 2026-09-02 CHANGES_REQUESTED is the only thing holding this off the train (reviewDecision still reads it; 23/23 checks green, regression diff ✅ on 97c5cb92, MERGEABLE). Both criticals and every warning were addressed in 02fe1bc3 (frozen 60-tuple route proof that runs on fetch-depth: 1; integration collection fixed; rglob on both invariant guards; dead rate-limit constants removed and the test re-pointed; public.py monkeypatch target unified; architecture.md paths; closing keyword). If anything there still reads wrong to you, say which and I'll take it — otherwise a dismiss or an approve unblocks it.

🤖 Generated with Claude Code

https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht

@dolho

dolho commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

/review — head 97c5cb92 vs merge-base bce0bbe1 — merge-safety pass

95 files, +11,107/−8,704, 12 commits. The split itself has had three passes (obasilakis 09-02, my two 09-08 self-reviews, the 09-09 fix commit) and I am not re-litigating the mechanics. This pass asks the question the 09-04 heads-up raised and that has been compounding since: what does merging this branch do to dev as it stands today?

[C1] The modify/delete conflict now spans 903 lines of dev, including a security fix, and none of it exists in the split packages (Confidence 9/10)

git merge-tree HEAD origin/devCONFLICT (modify/delete) on src/backend/routers/settings.py and src/backend/services/git_service.py (deleted here, split into routers/settings/ ×10 and services/git_service/ ×9). Since the merge-base, dev has changed those two files in six PRs — #2757 (ent#615), #2715, #2619, #2707, #2741, #2739 — for +903/−80 lines across settings.py, git_service.py and ops.py.

Spot-check of the branch's packages for the ent#615 hunk (the fleet-PAT fix: git_credential_helper, the credential-less remote URL, the stderr discriminator): absent. Same for the #2715 onboarding routes in settings.py. Git's default resolution of modify/delete keeps dev's file beside the new packages, and main.py imports whichever wins the import order — so the honest outcome of a naive merge is either the monolith shadowing the package (the split silently reverts) or the package shadowing the monolith (ent#615 and five other PRs silently revert). Neither is a "resolve the conflict" click; every hunk has to be re-ported by hand into the file it now belongs to, and the list grows with every dev merge — #2699/#2702 (open) touch routers/settings.py too.

This is not a defect in the diff as written. It is the reason the branch cannot be a train member in its current shape, and it is the same conclusion as the 09-04 sequencing note, now with a number on it.

What lands it

Two options, both real work, pick one:

  1. Re-port now, with a ledger. Merge origin/dev, resolve the two modify/delete conflicts by deleting dev's copies, then walk git log bce0bbe1..origin/dev -- routers/settings.py services/git_service.py commit by commit and re-apply each hunk into its new home. Record the mapping (commit → target file) in the PR body so the reviewer can check completeness against git diff bce0bbe1 origin/dev -- <file> rather than trusting the merge. tests/unit/test_ent615_* and the feat(onboarding): browser admin claim, one first-run overlay, credentials without a terminal (trinity-enterprise#580, #581, #582) #2715 onboarding tests are the executable proof that nothing was dropped; a green suite on the merged tree without them is not.
  2. Split the PR (the 09-04 suggestion): land the frozen route-set fixture first as its own PR, then one PR per module split, each small enough to re-port in an afternoon. Four PRs that each merge in a day beat one that re-conflicts every day.

Until one of those happens the standing CHANGES_REQUESTED is correct for the wrong reason — the 09-02 items are answered (02fe1bc3), but the branch is not mergeable without losing code, which is a stronger hold than any of them were.

Not re-raised

The 09-02 findings (three inert guards) are addressed in 02fe1bc3 and were re-reviewed on 09-08; the regression diff failure was a stale run, confirmed green on 97c5cb92; test_1028_settings_package.py executes the package's route set rather than grepping it.

Applying status-needs-fix (#2815). It clears on your next push.

@dolho dolho added the status-needs-fix PR has an unaddressed review/validation finding; cleared by the author's next push (#2815) label Sep 15, 2026
@vybe

vybe commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

merge-train: not on this train — the review gate, not the code

Recording why this was set aside, since it isn't a finding against the work.

  1. reviewDecision is still CHANGES_REQUESTED (@obasilakis, 2026-09-02). Your 2026-09-11 note is right that everything was addressed in 02fe1bc3, but a merge train can't dismiss another reviewer's block — that's the reviewer's call, and dev has enforce_admins: true with no bypass.

  2. It has since gone CONFLICTING. At 95 files and +11,107/−8,704 this is the largest thing in the queue by an order of magnitude, and it collides with essentially any co-rider. Even resolved, a batch containing it can't isolate a failure to a member — which is the one thing a train is for.

Neither is a quality judgment. The right path is a dismiss-or-approve from @obasilakis, then a rebase, then merge it alone rather than in a batch — a refactor this size wants its own green run and its own deploy, not shared attribution with five other PRs.

Also for the record: my queue scan flagged this PR for a "hold" and that was a false positive — the phrase matched was "the only thing holding this off the train", from your own comment asking to be unblocked. Nobody has put a hold on this.

🤖 Generated with Claude Code

dolho and others added 2 commits September 15, 2026 21:38
… 903 dev lines into the split packages

The modify/delete conflicts on `routers/settings.py` and
`services/git_service.py` are resolved by DELETING dev's monolith copies and
re-porting every hunk dev added to them since the fork into the file that
now owns it, symbol by symbol, with each function's body checked equal to
dev's modulo package qualification:

git_service (one dev commit, ent#615 / #2757 — the fleet-PAT fix):
  - `_AUTH_PATTERNS` marker            -> conflicts.py
  - `_git_remote_url` removed, `_remote_seturl_subcommand` docstring,
    `_credentialless_remote_url`, `rebind_origin_and_push` (root push +
    credential in the exec env), `update_remote_pat` (env write, not URL)
                                        -> remotes.py
  - the credential-helper install + embedded-token sweep block
    (`write_container_github_pat`, both alarms, `scrub_git_remote_tokens`,
    the fleet sweep, `spawn_git_remote_token_scrub`, all `_SCRUB_*`)
                                        -> NEW token_scrub.py (remotes.py
    would otherwise sit at 821 lines, over the threshold the split exists for)
  - `_agent_can_push`, `_agent_has_write_credentials` docstring,
    `sync_to_github`, `reset_to_main_preserve_state`   -> sync.py
  - `initialize_git_in_container` (seeds before writing a remote)
                                        -> provisioning.py
  Package `__init__` re-exports every new name; the duplicate
  `REBIND_PUSH_TIMEOUT_S` the hunk would have introduced is dropped.

settings (five dev commits — #2715, #2619, #2707, #2741, #2739):
  - 11 changed routes replaced in place across flags/credentials/
    integrations/generic
  - 11 new symbols placed beside their dev-order predecessors; the #2715
    Resend/Gemini routes + their two helpers go to NEW provider_keys.py
    (credentials.py would otherwise reach 1,045 lines), included on the
    package router right after `credentials` and before `generic`
  - `_ANTHROPIC_KEY_ALIASES` / `_adopt_after_instance_key_removed` reached
    from generic.py through the sibling module object, per the package rule

ops: `_format_model_name`'s #2739 `claude-fable-5-1` entry lands in
`ops_costs_service.py`, where the split moved the function; the #2726
test imports from there.

Dev's tests that patch monolith attributes are re-pointed the way the
split re-pointed every earlier one: the ent#615 exec recorder is installed
on each execing sibling and `_detect_git_dir` on `gitignore`; #2572's `db`
fake on `credentials` and `generic`; ent#553's source read on `flags`;
#1677's emitter allowlist and the ent#615 source reads on `token_scrub`.
`_PRE_SPLIT_ROUTES`' post-split allowlist records the six #2715 routes; the
git_service import-surface pin drops `_git_remote_url` (gone by design) for
its ent#615 replacements.

Content conflicts: `backend.md` (dev's facts under the package names),
`test_ent123_tokenless_clone.py` (dev's helper patch, on `gs.sync`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…lier one does (#1028)

Five files landed on dev after the fork with patch targets and source reads
on the monoliths. Re-pointed the same way the split re-pointed the rest:

- test_ent582_platform_keys: `is_claude_auth_configured` /
  `connect_agents_to_first_credential` on `settings.credentials`,
  `platform_keys_service.check_resend_key` on `settings.provider_keys`
- test_2695_stt_capability_probe: `_elevenlabs_settings_state_with_capability`
  on `settings.integrations`
- test_2691_public_url_reachability: the save-path source read on
  `settings/generic.py`, the flag-surface read on `settings/flags.py`,
  `update_setting`/`db`/`platform_audit_service` on `generic`
- test_github_init_push: the exec recorder also installed on
  `git_service.token_scrub`, which the ent#615 seed now runs through
- routers/settings/generic.py: the #2572 hook reaches `credentials` through
  an absolute function-local import — `test_2216_backup_observability` and
  `test_2572` load this module in isolation via `spec_from_file_location`,
  where a module-level relative import raises at collection

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
@github-actions github-actions Bot removed the status-needs-fix PR has an unaddressed review/validation finding; cleared by the author's next push (#2815) label Sep 15, 2026
@dolho

dolho commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on dev70a582ab4 (the merge) + 9c3466264 (test re-points). Now MERGEABLE, and the modify/delete conflicts are resolved the honest way: dev's monolith copies deleted, every hunk dev added since the fork re-ported into the file that now owns it, then checked symbol-by-symbol against dev.

The ledger (what a reviewer can verify with git diff bce0bbe1 origin/dev -- <file>)

services/git_service.py — one dev commit, ent#615 / #2757 (the fleet-PAT fix), 12 hunks, +635:

dev hunk now lives in
_AUTH_PATTERNS + TRINITY_GIT_NO_CREDENTIAL marker conflicts.py
_git_remote_url removed; _remote_seturl_subcommand docstring; _credentialless_remote_url; rebind_origin_and_push (root push, credential in exec env); update_remote_pat (env write, not URL) remotes.py
write_container_github_pat, both scrub alarms, scrub_git_remote_tokens, sweep_fleet_git_remote_tokens, schedule_…/spawn_…, every _SCRUB_* new token_scrub.pyremotes.py would otherwise sit at 821 lines, over the threshold the split exists for
_agent_can_push, _agent_has_write_credentials docstring, sync_to_github, reset_to_main_preserve_state sync.py
initialize_git_in_container (seeds before writing a remote) provisioning.py

routers/settings.py — five dev commits (#2715, #2619, #2707, #2741, #2739), +340: 11 changed routes replaced in place (flags / credentials / integrations / generic); 11 new symbols placed beside their dev-order predecessors — the #2715 Resend/Gemini routes + two helpers in new provider_keys.py (credentials.py would reach 1,045 lines), included right after credentials and before generic.

routers/ops.py — the #2739 claude-fable-5-1 label lands in ops_costs_service._format_model_name, where the split moved the function.

Completeness, machine-checked, not eyeballed: every top-level symbol of dev's two monoliths resolved to exactly one package file — 109/109 (git_service) and 88/88 (settings) — with each body equal to dev's modulo sibling-module qualification (gitignore._detect_git_dir(...)) and one line-wrap. The ent#615 hunk that a naive resolution would have silently reverted (git_credential_helper, the credential-less remote, the root-exec push) is present and its own suite is green (test_ent615_token_free_remotes.py 66/66, test_ent615_credential_helper_parity.py).

Dev's post-fork tests

Five files patch monolith attributes / read monolith source; re-pointed the way the split re-pointed the other ~90 (gs.sync, gs.token_scrub, settings.credentials, settings.generic, settings.flags). _PRE_SPLIT_ROUTES' post-split allowlist records the six #2715 routes; the git_service import-surface pin drops _git_remote_url (gone by design) for its ent#615 replacements. One package change for the isolated-loader tests: generic.py reaches the #2572 hook through a function-local absolute import, since test_2216/test_2572 load it via spec_from_file_location where a relative import raises at collection.

Full unit suite on the merged tree

16,354 passed / 15 failed / 30 skipped — the 15 are all the ::ffff: IPv4-mapped family from a local Python 3.12 vs the pinned 3.13; zero from the port (first run had 17, every one a monolith patch target, all in 9c3466264).

On the gate

Agreed with the 17:11 note on both counts: this merges alone, never batched, and the standing CHANGES_REQUESTED is @obasilakis's to lift — the 09-02 items were answered in 02fe1bc3, and the thing that has actually held it since (the modify/delete drift) is now gone. @obasilakis, re-review requested; the ledger above is the shape of the question.

Removing status-needs-fix by hand — #2819 is on dev, so the next push clears it automatically from here.

sim and others added 2 commits September 16, 2026 15:15
…ers/settings/

dev changed the deleted monolith twice since the last merge (0bddbc9):

| dev hunk | now lives in |
|---|---|
| #2836 (ent#438) retire `workspace_available` + its docstring bullet | `routers/settings/flags.py` |
| #2702 (#2696) `describe(cap, api_key=...)` + comment | `routers/settings/integrations.py` |

dev's copy of `routers/settings.py` is removed, as in the previous merge.
The #2836 port is load-bearing: dev removed
`settings_service.is_workspace_enabled`, so the unported `flags.py` would
raise AttributeError on GET /api/settings/feature-flags whenever voice is
available.

Checked with an AST pass: all 79 functions in dev's monolith exist in the
package. Only three bodies differ, and those are the split's own
cross-module references (`credentials.mask_api_key`, and
`credentials._ANTHROPIC_KEY_ALIASES` / `_adopt_after_instance_key_removed`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`test_workspace_flag_retired` patches `settings_service`, `telemetry_sharing_service`
and `db` on the module it calls `get_public_feature_flags` from, and
`test_2696_stt_provider_errors` calls `_elevenlabs_settings_state_with_capability`.
Both imported the flat `routers.settings`. Now they import the `flags` and
`integrations` submodules, like the earlier re-points in 9c34662, so the
patches land on the globals the handlers actually read.

Full unit suite on this tree: 16468 passed, 32 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vybe

vybe commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-16: two commits pushed to this branch. The branch had gone CONFLICTING again. dev changed the deleted routers/settings.py twice since your 0bddbc95 merge. I re-ported both hunks the same way your ledger did.

23445e28: merge dev, with the re-ports

dev hunk now lives in
#2836 (ent#438) retires workspace_available and its docstring bullet routers/settings/flags.py
#2702 (#2696) describe(cap, api_key=...) and its comment routers/settings/integrations.py

dev's monolith copy is removed. The #2836 port is load-bearing. dev deleted settings_service.is_workspace_enabled, so an unported flags.py would raise AttributeError on GET /api/settings/feature-flags whenever voice is available. An AST pass over dev's monolith finds all 79 functions in the package. Only three bodies differ, and those are the split's own cross-module references (credentials.mask_api_key, credentials._ANTHROPIC_KEY_ALIASES and _adopt_after_instance_key_removed).

f473d5ef: dev's two new settings tests reach the submodules

test_workspace_flag_retired and test_2696_stt_provider_errors imported the flat routers.settings. They now import flags and integrations, the same way as your re-points in 9c346626, so their patches land on the globals the handlers read.

Local result on this tree: the full unit suite gives 16468 passed, 32 skipped, 0 failed. The four test_1028_* package tests and test_ent500_public_turn_no_pii pass, and the frozen route-set proof runs rather than skipping.

…low the moved ops code (#1028)

merge-train validation findings. Collection was fixed earlier; these are the
runtime half.

- `tests/integration/test_circuit_breaker.py`: 19 `monkeypatch.setattr(agent_client, ...)`
  calls and 34 `agent_client.CIRCUIT_*` reads now target
  `services.agent_client.circuit`, which reads its own globals. Patching the
  package re-export changed nothing, and `_get_circuit_redis` is not
  re-exported, so it raised AttributeError. Against a fakeredis server:
  8 failed / 26 passed before, 34 passed after (dev: 34 passed).
- `tests/git_sync/test_s5_conflict_classifier.py` loads `git_service/conflicts.py`,
  because the flat `git_service.py` no longer exists.
- `tests/git_sync/test_s7_reserve_instance_id.py` patches
  `check_remote_branch_exists` and `db` on `git_service.provisioning`, where
  `reserve_and_generate_instance_id` looks them up. Both git_sync files:
  33 passed.
- `tests/unit/test_1917_stack_trace_exposure.py`: the raw `str(e)` ban now
  also scans `services/fleet_ops_service.py` and `services/ops_costs_service.py`,
  where the ops handler bodies moved. Neither file has any hits today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vybe

vybe commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-16: one more commit, b246c438. This finishes Critical 2 from the 09-02 review. The collection half was fixed earlier, and this is the runtime half.

  • tests/integration/test_circuit_breaker.py. 19 monkeypatch.setattr(agent_client, …) calls and 34 agent_client.CIRCUIT_* reads now target agent_client.circuit, which reads its own globals. Patching the package re-export changed nothing. _get_circuit_redis is not re-exported, so that patch raised AttributeError. Against a fakeredis server, 8 tests failed before the change and all 34 pass after it, the same as dev.
  • tests/git_sync/test_s5 and test_s7. S5 loads git_service/conflicts.py now that the flat file is gone. S7 patches check_remote_branch_exists and db on git_service.provisioning, where the helper looks them up. Both files pass, 33 tests. No CI workflow runs this tier, only run-full.sh.
  • test_1917_stack_trace_exposure. The raw str(e) ban now also scans fleet_ops_service.py and ops_costs_service.py, where the ops handler bodies moved. Neither file has any hits today.

Review status. Behaviour equivalence was checked by AST against dev:

  • git_service: 121 of 121 symbols present.
  • agent_client: 46 of 46 identical.
  • settings: 88 of 88 present, and all 67 routes are identical on path, methods, dependency tree and response model.
  • The auth gates in ops and public are unchanged, and /cso --diff has no findings.

Every item in the 09-02 review is now addressed, except the closing keyword. "Related to #1028" stays, because #1028 still covers main.py and create_agent_internal.

Suggestions not acted on:

  • The unused SKILLS_AUTOMATION_KEYS / LEGACY_SKILLS_LIBRARY_KEYS imports copied into the settings modules.
  • The stale "monkeypatch agent_client._get_circuit_redis" docstring at circuit.py:176.
  • The module counts in architecture.md, which should read nine git_service modules and eleven settings modules.

@vybe
vybe dismissed obasilakis’s stale review September 16, 2026 15:33

merge-train 2026-09-16: every item in this review is addressed on head b246c43. C1: the frozen route-set proof runs in CI. C2: collection was fixed in 02fe1bc, and the runtime half in b246c43 (circuit breaker integration goes from 8 failed to 34 passed on fakeredis). All warnings are resolved except the closing keyword, which is deliberate because #1028 still covers main.py and create_agent_internal. An AST check against dev confirms equivalence: git_service 121/121, agent_client 46/46, settings 88/88 with all 67 routes identical. /cso --diff has no findings, and the regression diff shows no new failures. Details are in the PR comments.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train 2026-09-16: validated at lane C (/validate-pr, /cso --diff, plus the author's own /review). Re-merged dev, with #2836 and #2702 re-ported into the settings package. The integration and git_sync patch targets are fixed, and the #1917 guard now follows the moved ops code. The full unit suite passes locally (16468 passed, 0 failed), CI's regression diff shows no new failures, and the AST equivalence check against dev is clean. No file overlap with the other members, so it merges individually.

@vybe
vybe merged commit 5c4dc4b into dev Sep 16, 2026
36 of 37 checks passed
obasilakis pushed a commit that referenced this pull request Sep 17, 2026
…ilures in test_ent615 (0.9.5 M1) (#2861)

* fix(tests): evict the whole git_service package family, not just its parent — 13 leaked failures in test_ent615 (0.9.5 M1)

#2487 made `services.git_service` a package. Three test helpers still did
`sys.modules.pop("services.git_service")` before re-importing it with stubbed deps.
Evicting only the parent leaves `services.git_service.<sub>` cached, and Python's
import re-binds a cached submodule onto a NEW parent only on first load — so the
re-imported package had no `.token_scrub` / `.conflicts` attribute, and every later
`git_service.<sub>` read in test_ent615_token_free_remotes.py failed with
`AttributeError: module 'services.git_service' has no attribute 'token_scrub'` (13
failures on dev; the file passes alone). Evict the family (the shape
test_reset_preserve_state_guardrails.py already uses) in test_1704_git_service_plugins,
test_data_paths_allowlist and test_data_paths_gitignore, and make 1704's autouse
restore family-aware for the same reason.

Reproduced: `pytest unit/test_data_paths_allowlist.py unit/test_ent615_token_free_remotes.py`
→ 13 failed; after: 118 passed in both orders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tests): the fourth polluter + a named regression guard for #2859

#2859 names four files; the first commit covered three. test_2075_detect_git_dir.py
swept the family but recorded only the package key for undo, so the submodules its
own re-import CREATED outlived monkeypatch's teardown — same half-state, same 13
failures. It now carries the lint_sys_modules.py-sanctioned pair
(_STUBBED_MODULE_NAMES + an autouse _restore_sys_modules) restoring the whole family;
the two data_paths files get the same family-aware restore as 1704 (their parent-only
restore fixture was a second leak behind the pop).

tests/unit/test_2859_git_service_eviction.py: (1) the mechanism on a throwaway
package — parent-only eviction loses the submodule attribute, family eviction rebinds
it; (2) an AST guard over tests/unit that fails on `sys.modules.pop("services.git_service")`,
`del sys.modules["services.git_service"]` or `monkeypatch.delitem(sys.modules,
"services.git_service")` outside the family shape (self-tested on the exact lines the
polluters carried; proven red by reintroducing the pop on a copy).

Issue's repro table (-p no:randomly, each file then ent615): 102 / 101 / 93 / 101 passed,
0 failed. lint_sys_modules.py: no new violations.

Fixes #2859

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants