Repository navigation
feat(OMN-18412): the CI runner routing decision is a COMPUTE node, declared by its contract - #3604
Conversation
…clared by its contract Operator direction, 2026-09-15: contract-driven, OmniNode primitives, no one-offs. The per-run routing decision was a 900-line script with its thresholds in a config file. It is now node_ci_runner_route_compute: a contract, a declarative node, and one stateless definition-B handler taking a typed request and returning a typed decision. WHERE IT LIVES AND WHY. omnibase_infra owns the CI routing surface already -- the fleet inventory, the hosted-workflow list, the routing audit, the fleet canary and the summary gate are all here, and the repository holds 125 nodes including the compute archetypes. omnimarket owns the runtime-deployment domain, which is a different question from where a CI job executes. WHAT THE CONTRACT DECLARES: the label sets, every threshold, the reasons a hosted verdict may be reversed for a repository that may not run hosted, and the private-repository refusal. Parsed into a typed policy whose fields are all required, so a contract missing one fails at load naming it rather than at the first run that needed it. No threshold is read from an environment variable and none is restated in the old config file -- two copies are two policies that agree until someone edits one, and a test now fails if one reappears. THE FLEET SIZE IS READ FROM THE FLEET INVENTORY, not restated. That is not tidiness: the degraded floor was the literal 60, chosen as two thirds of the 88-runner fleet of the time, and the fleet was capped to 60 the same day this was written. The literal had become equal to the whole fleet, so one runner going offline would have refused the fleet on every run and reported it as an ordinary return to GitHub-hosted. As a fraction of the declared inventory the same two thirds tracks the cap. The script keeps exactly what a boundary should: the probes, request assembly and the CLI. It cannot drift from the decision because it no longer contains one. Its node imports are function-local by necessity -- the self-hosted lab-load probe imports this module on the fleet image's bare interpreter, which has neither PyYAML nor pydantic. An operator force input lands with it, subject to every safety rule rather than a bypass of them: fleet cannot widen past the seam ceiling or take a fork pull request, and hosted is still refused for a private repository. It exists so the kill switch a static seam value provided survives per-run routing. Not used: the sanctioned local node CLI. Measured today, it refuses to dispatch unless the workspace venv carries every sibling package installed from git, which on a hosted runner is minutes of install on the critical path of every run for a decision that takes milliseconds. The same handler executes either way; only the dispatch machinery differs. Recorded in the workflow beside the invocation rather than left as an unexplained choice. Proven, not asserted: 266 tests pass across the decision, the wiring, the incident replay, the routing audit and the new contract-binding suite. Four live dry runs through the node -- private plus saturation gives the fleet, public with identical inputs gives hosted as the control, public with an idle fleet and a fresh lab record gives the fleet, and the same run forced hosted gives hosted. The contract suite asserts the declared reversal reasons are real enum members and exclude every trust reason, which is the one defect nothing else can see: a misspelling there silently stops reversing. No Actions variable is written. Routing stays inert wherever a seam reads hosted.
The unit suites cover the handler and the contract suite covers the node's shape. Neither covers the seam that decides real placement: a subprocess reading the committed contract and fleet inventory off disk, writing the four lines a workflow consumes, and signalling a refusal through its exit status. That seam has broken twice in ways every unit test survived. A JSON seam value interpolated into a quoted shell argument lost its inner quotes and reached the module as non-JSON, so every decision came back unparseable and looked exactly like a correctly inert mechanism. And a refusal that exits non-zero is worthless if the workflow's fail-closed floor overwrites its labels first. Six cases, each against the real contract: the fleet path, saturation, the seam surviving the shell with an unparseable-seam positive control so the assertion discriminates, a private repository refused a hosted downgrade, a refusal that exits 3 and still leaves both a record and the labels line the floor checks for, and the operator override in both directions.
|
| Surface | Meaning | Blocks merge? |
|---|---|---|
| Review threads | Per-finding, posted by the reviewer | No (informational) |
Hostile Review Thread Gate |
Deterministic: unresolved hostile-reviewer threads exist | Fails until resolved (not yet a required context) |
degraded verdict |
Fewer than 2 models succeeded (infra) | No |
Powered by omniintelligence.review_pairing.cli_review — multi-model adversarial review: qwen3-review, qwen3-review-b, glm-review (OMN-8468/OMN-8524/OMN-17492)
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: glm-review
Models failed: codex
New finding threads: 3
Deduped (already posted on this PR): 0
Nit-level findings suppressed: 1
The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.
Findings not anchored to a changed file
-
[CRITICAL] hostile-reviewer (glm-review)
Refusal path emits empty runs_on against a fail-closed workflow floor | The decision model states runs_on is EMPTY on a refusal and that a refusal 'names no placement'. The boundary script then exits non-zero on refusal. The workflow's fail-closed floor only writes fallback labels when GITHUB_OUTPUT lacks a labels= line; if the script wrote a partial decision record with labels= (empty) before exiting non-zero, or if the boundary handler emits an empty array, a consumer's runs-on resolves to an empty list a
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
force input accepted as free-form workflow_dispatch string | The new
forceworkflow input is a plain string with default 'auto' and is interpolated directly into the CLI invocation via --force. The diff does not show validation of arbitrary values (e.g. 'fleet; rm -rf') before argparse consumes it, and the documentation claims FLEET 'still cannot widen past the seam ceiling', yet EnumCIRunnerRouteForce.FLEET semantics are nowhere demonstrated in the shown handler excerpt: where in the elimination does FLEResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
min_online_fraction against expected_count can exceed observed fleet cap semantics | The policy moves from min_online_runners: 60 to min_online_fraction: 0.67 against fleet_expected_count from config/runner_fleet.yaml. The rationale cites the fleet being 'capped to 60', making the old literal equal to the fleet size. But 0.67 * 60 = 40.2, so the floor silently drops from 60 required-online to 41 required-online, a 32% relaxation of a safety threshold presented as an equivalent re-derivation. The rationale a
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Decision reads repository visibility at decision time only in the boundary; replay determinism claim unverified for staleness | The contract advertises 'Stateless and deterministic: every observation is probed at the boundary and handed in, so a recorded decision replays without a network.' But the boundary probes visibility live and the evidence model records it; that part replays. However the fleet_expected_count is read from a live config file at request-assembly time, and the policy comes from contract.
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
parse_labels imports json inside function; falls to PROBE_ERROR reason for a config fault | parse_labels returns None for an unparseable seam and _eliminate maps None to reason PROBE_ERROR with detail seam_unparseable. A config fault is thereby counted in the same reason bucket as a transient probe failure. Under the new refusal logic, PROBE_ERROR is listed in capacity_downgrade_reasons, meaning an unparseable seam for a private repository is REVERSIBLE: the private-repo guard reverses probe_error, converti
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
G2 guard ordering rationale contradicts the elimination; UNKNOWN visibility path untested in shown coverage | The handler docstring asserts G2 runs after G1 'precisely because G1 emits hosted, so running it earlier would leave the one path that ignores the elimination able to break the ruling.' This argument is incoherent: G2 on a private repository must also intercept a self_hosted answer that would widen hosting obligations? Per the reason enum, PRIVATE_REPO_NO_HOSTED_DOWNGRADE reverses capacity-hosted ba
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
Findings demoted from threads (anchor rejected)
-
[MAJOR] hostile-reviewer (glm-review)
Boundary script pinned to pydantic 2.13.4 diverges from project dependency resolution | The workflow installs pydantic==2.13.4 and PyYAML==6.0.2 as exact pins 'TWO PINNED PACKAGES, NOT THE PROJECT ENVIRONMENT'. The handler and models are then imported from the checked-out tree against this stripped environment. Any transitive import in omnibase_infra.enums (EnumHandlerType, EnumHandlerTypeCategory) or omnibase_core.nodes pulled in by the handler's import chain must survive with only these two packages; if o
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Workflow adds setup-python step to a 2-minute-capped critical-path job | The route job has timeout-minutes: 2 and now adds actions/setup-python@v5 plus pip install of two packages on every consuming run. setup-python cache is not configured; cold downloads of the interpreter toolchain can approach a minute on hosted runners, and any slowness here cascades to all consumers since the route job gates scheduling. The stated motivation is avoiding a minutes-long full dependency sync, which the two pins solve, bu
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Audit regex now matches both output names but audit of runs_on consumers lacks a corresponding wiring test | ROUTE_CONSUMER_RE is widened to accept needs.X.outputs.(labels|runs_on), and the wiring test asserts the reusable workflow exposes both outputs. No shown test covers the audit script recognizing a consumer that uses runs_on, i.e. the regex change itself is untested in the visible diff. A regression narrowing the regex later would reclassify runs_on consumers as unrouted with no failing test. | Eviden
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: glm-review
Models failed: codex
New finding threads: 1
Deduped (already posted on this PR): 0
Nit-level findings suppressed: 2
The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.
Findings not anchored to a changed file
-
[CRITICAL] hostile-reviewer (glm-review)
Workflow fail-closed floor contradicts and defeats the refusal semantics | The boundary script's refusal path (BLOCKED decision, non-zero exit) deliberately emits no runs-on, per the decision model: 'EMPTY on a refusal: a refusal names no placement.' But the workflow's FAIL-CLOSED FLOOR triggers whenever GITHUB_OUTPUT lacks a labels= line, which is exactly the refusal state, and writes labels=["ubuntu-latest"], runs_on=["ubuntu-latest"], decision=hosted. The result: a private repository's refusal is silentl
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Exit status of the decision script is captured and never checked | The diff shows
|| rc=$?capturing the exit code, andset -uo pipefail(no-e), but no visible use ofrcin the step. Ifrcis not subsequently tested, a refusal or any non-zero exit is silently swallowed and only the floor's heuristic runs, making the exit-code contract added by this PR dead code in production while the new integration test asserts on it. | Evidence: `python3 scripts/ci/runner_route_decision.py "${args[@]}" || rc=$Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Minimal pip install cannot satisfy the handler's import graph | The workflow installs only pydantic==2.13.4 and PyYAML==6.0.2 and claims the decision needs 'the typed models and a YAML parser and nothing else.' But handler_ci_runner_route.py imports
from omnibase_infra.enums import EnumHandlerType, EnumHandlerTypeCategory, which pulls the omnibase_infra package root and, transitively, omnibase_core. omnibase_infra is a full infra package;omnibase_infra/__init__.pyand the enums module commonly carry siResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Test and wiring code hardcodes fleet_expected_count literals that duplicate the inventory | The PR's stated rationale for min_online_fraction is that count literals go stale when the fleet resizes, and fleet_expected_count is declared to come 'from config/runner_fleet.yaml rather than restated as a literal.' Yet test_runner_route_wiring.py and test_incident_replay_omn18031.py pass
fleet_expected_count=88as a literal, and test_runner_route_decision.py sets FLEET_EXPECTED = 60, none of them reading the comResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Dual outputs labels and runs_on institutionalize the drift the PR elsewhere eliminates | The PR's stated philosophy is 'two copies of a threshold are two policies that agree until the day they do not,' yet it adds a second output (runs_on) that is byte-identical to labels by construction and maintained as a parallel path in the workflow floor, the audit regex, and the contract. The audit regex change exists solely to tolerate both spellings. A future edit to one emission point and not the other yields a rou
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Wiring test indexes workflow mapping with boolean True | The modified test reads
_workflow("runner-route-reusable.yml")[True]["workflow_call"], replacing the previous_triggers(...)["workflow_call"]shape. Indexing a parsed workflow document with True suggests the helper changed its return shape (perhaps keying triggers on the presence of workflow_call), but as written it is opaque and brittle; if the helper returns the raw document this raises KeyError, and if it returns a dict keyed by booleans the coResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
reason_wire rendering is ambiguous when reason_detail contains a colon | reason_wire emits f"{reason.value}:{reason_detail}" and the saturation monitor counts these strings while the audit matches consumers against them. A reason_detail containing a colon (probe error classes are themselves colon-qualified elsewhere, e.g. probe_error:seam_unparseable appears in prose and the floor) makes the wire form non-injective across (reason, detail) pairs, so a monitor keyed on the full string can conflate classes. |
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
Findings demoted from threads (anchor rejected)
-
[MINOR] hostile-reviewer (glm-review)
Actions referenced by mutable tag, not SHA | The diff adds actions/setup-python@v5 to a workflow that elsewhere demonstrates zizmor awareness (persist-credentials: false for artipacked). Tag-pinned third-party actions are a known supply-chain vector; checkout@v4 has the same pre-existing exposure but setup-python is newly added by this diff. pip install also uses version pins without hash verification, acceptable for the two packages but inconsistent with the workflow's stated security posture. | Evidence:
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
#9745) * evidence: OCC companion pass 1 for OmniNode-ai/omnibase_infra#3604 * evidence: OCC companion self-bind for #9745 --------- Co-authored-by: node-occ-companion-effect <occ-companion-effect@omninode.ai>
…ails every caller at startup Measured on run 35037012235: ci.yml and the routing probe both concluded failure with ZERO jobs, and the run title fell back to the file path instead of the workflow name. That is the whole visible symptom. No check reports on a run that never started, so the pull request showed 62 green checks, no red one, and a merge state of BLOCKED with nothing to point at. The cause was an illustrative snippet I put in the new runs_on output's DESCRIPTION. Actions evaluates expressions inside a workflow_call outputs block, where the needs context does not exist, so the reusable failed to compile and took every calling workflow with it. The description is now prose. A test asserts no workflow_call input or output description in either routing workflow carries an expression, because this failure is invisible to every check-based gate by construction.
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: glm-review
Models failed: codex
New finding threads: 3
Deduped (already posted on this PR): 0
Nit-level findings suppressed: 0
The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.
Findings not anchored to a changed file
-
[MAJOR] hostile-reviewer (glm-review)
Refusal decision collapses to hosted labels at the workflow boundary | The decision model declares BLOCKED with an explicitly empty runs_on ('a refusal names no placement, which is not the same as hosted'), and the boundary script exits non-zero on refusal. The reusable workflow's fail-closed floor then fires: it checks only that GITHUB_OUTPUT contains a labels= line, finds none, and writes labels=["ubuntu-latest"], runs_on=["ubuntu-latest"], decision=hosted, reason=probe_error:module_unavailable. Net effec
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Forced fleet semantics vs FORCED_FLEET reason are internally inconsistent | The workflow input description, the force enum docstring, and the contract all state force=fleet 'cannot widen past the seam ceiling'. But EnumCIRunnerRouteReason includes FORCED_FLEET and the request model permits force=fleet with auto seam semantics. If force=fleet is bounded by a hosted-only seam (the documented live value), then force=fleet is a no-op in the one situation an operator would use it (the seam is a static kill switc
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Unpinned-at-hash PyPI install on the routing critical path | The route job installs pydantic==2.13.4 and PyYAML==6.0.2 from PyPI on every run with no hash pinning and no cache. Version pins constrain the API but not the artifact: a compromised sdist/wheel of either package executes with workflow privileges on every consuming run's critical path. The workflow otherwise demonstrates zizmor-awareness (persist-credentials: false), so the gap is conspicuous. | Evidence: run: pip install "pydantic==2.13.4" "PyYAM
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Fleet inventory literals (60, 88) duplicated across test suites | The stated rationale for min_online_fraction is that literal runner counts go stale on resize, and the request model takes fleet_expected_count from config/runner_fleet.yaml to avoid restated literals. The tests reintroduce the hazard: test_runner_route_decision.py pins FLEET_EXPECTED = 60, while test_runner_route_wiring.py's _decide_via_node and test_incident_replay_omn18031.py hardcode fleet_expected_count=88. A fleet resize will desynchron
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Probe parse_labels imports json inside the function | parse_labels performs
import jsoninside the function body while the module already imports stdlib modules at scope (datetime, urllib, etc.). Given the file's own history of conditional-import reasoning documented in the removed hunk, a function-local import here looks deliberate but is unexplained; it also defeats static dependency analysis and repeats import lookup per call. Trivial, but in a file whose stated purpose is being the auditable I/O boundResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
Findings demoted from threads (anchor rejected)
-
[MINOR] hostile-reviewer (glm-review)
Fail-closed floor checks labels but not runs_on | The floor condition is
! grep -q '^labels=' "${GITHUB_OUTPUT}". If the script writes labels= then crashes before writing runs_on=, the floor does not fire and runs_on resolves to the empty string at consuming call sites, which per the wiring test's own analysis 'fails to schedule rather than failing loudly'. The new dual-output contract doubles the partial-write window. | Evidence: if [ ! -s "${GITHUB_OUTPUT}" ] || ! grep -q '^labels=' "${GITHUB_OUTPUT}";Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Force input accepts arbitrary strings with no workflow-level validation | The force input is
type: stringwith nochoicerestriction. Any value (typo included) is passed through to --force. Downstream a StrEnum ValidationError in build_request is presumably caught by the boundary and reported as a probe_error-grade hosted fallback, so a typo in a caller's with: force: fleet silently disables the override with a misleading reason. Validation belongs at the input boundary where it is visible in the ActionResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Constant policy field models configuration as tunable | private_repo_hosted_placement is pattern-constrained to the single literal 'refuse'. A one-valued enum declared as configuration adds a parse surface (a contract edit passing validation still changes nothing) and implies a second value is contemplated when the description says the ruling admits none. Either accept the field with a test asserting its sole permitted value, or model the ruling as a boolean capability / fold it into the refusal policy. As
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
…s green while inert Measured on run 35039101488. The route job installed two pinned packages -- the models need only pydantic and a YAML parser -- and then failed with `No module named 'omnibase_core'`, fell through to its fail-closed floor, and reported SUCCESS carrying `reason=probe_error:module_unavailable`. Both consumer jobs scheduled hosted. Nothing was red. The mechanism was completely inert and the only evidence was a reason string in an artifact. The cause is not this node: importing anything under the package executes its __init__, which eagerly pulls the database and broker surfaces, so no node can be imported without the project's dependency set. Loading modules by file path around that would be bespoke plumbing for one job; syncing the environment with the repository's own shared setup action is what every other job here does. Both call sites now use that action and run the module inside the synced environment. The job's ceiling goes from 2 to 5 minutes -- the bound the wiring suite already codified -- rather than wider, because this job is on the critical path of every consuming run and a sync that stops fitting should fail rather than silently cost every pipeline minutes. A test asserts the setup step exists and precedes the decision at both call sites. That is the only durable guard: the floor exists precisely so a broken router still hands the run a usable runs-on, which is why nothing else can see this failure.
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: glm-review
Models failed: codex
New finding threads: 2
Deduped (already posted on this PR): 0
Nit-level findings suppressed: 2
The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.
Findings not anchored to a changed file
-
[MAJOR] hostile-reviewer (glm-review)
Refusal exits non-zero; workflow fail-closed floor may overwrite it with hosted | The PR introduces exit-status-3 refusals where runs_on is deliberately empty. The workflow's shell runs the decision with
|| rc=$?and then applies a 'FAIL-CLOSED FLOOR' that writes a usable runs-on when GITHUB_OUTPUT is empty. An empty output is exactly what a refusal produces. If the floor is not conditional on the refusal exit code, the refusal is silently converted to a hosted placement, reintroducing the very behavior tResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
force input is an unvalidated free-form string on workflow_call | The new
forceworkflow_call input istype: stringwith default auto and no allowed-values enforcement at the Actions layer. Any caller of the reusable workflow can pass an arbitrary string. The handler presumably maps unknown values to AUTO, which would make a typo like 'flet' a silent no-op override, contradicting the repo's own no-silent-default rule (CLAUDE.md rule 8 as cited in this PR). Additionally, in pull_request contexts the forcResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Test indexes workflow mapping with boolean True | The rewritten output-contract test reads
_workflow("runner-route-reusable.yml")[True]instead of a named trigger key. A boolean key lookup against the parsed workflow mapping is either a latent KeyError/TypeError or an artifact of how _workflow normalizes its return; either way it is fragile and will break silently if the loader changes, and the test would fail for the wrong reason, muddying the contract it pins. | Evidence: outputs = _workflow("runner-rouResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Heavy dependency pull onto the routing critical path | The fix for the inert router is a full project environment sync (
--frozeninstall of the entire dependency set, including database and broker surfaces) on a job that sits ahead of every consuming run, with timeout raised 2 to 5 minutes to accommodate it. The stated alternative, loading the node modules by path or trimming the package's eager init imports, was dismissed as 'bespoke plumbing', but the eager package init is the root cause and isResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
Findings demoted from threads (anchor rejected)
-
[MAJOR] hostile-reviewer (glm-review)
probe_error in capacity_downgrade_reasons conflates trust and capacity | The policy's reversal list includes probe_error, and the seam_unparseable path returns PROBE_ERROR. The stated invariant is that only 'the fleet is unavailable for THIS run' reasons may be reversed, because reversing a trust reason places untrusted code on the fleet. probe_error is not purely a capacity reason: it covers seam unparseability and any probe fault, including one caused by a corrupted or attacker-influenced input. Reversing
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
FLEET_EXPECTED hardcoded to 60 in tests undermines the fraction rationale | The PR's central policy change is that min_online_runners becomes min_online_fraction specifically because a literal count goes stale when the fleet is resized, with expected_count read from config/runner_fleet.yaml. The behaviour test suite pins FLEET_EXPECTED = 60 rather than reading the committed inventory, so tests exercising the degraded-floor threshold diverge from the shipped inventory and would not detect a runner_fleet.yaml
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
A contract-backed node is discoverable through the onex.nodes entry points, and the entry-point checker holds the generated section to the set of node directories on disk. Adding the node without its entry point left that test red -- correctly: a node the runtime cannot resolve by name is a node only its own importer can reach. Generated with the repo's generator against this worktree rather than hand-edited, so the section stays byte-identical to what the checker regenerates.
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: glm-review
Models failed: codex
New finding threads: 4
Deduped (already posted on this PR): 0
Nit-level findings suppressed: 1
The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.
Findings not anchored to a changed file
-
[CRITICAL] hostile-reviewer (glm-review)
Refusal path exits non-zero, contradicting the fail-closed contract for consuming workflows | The new design states a refusal (private repository whose only permitted placement is hosted) exits non-zero and emits an empty runs_on. The reusable workflow excerpt shows the invocation guarded by
|| rc=$?followed by a fail-closed floor that writes labels when output is empty. Either the floor overwrites the refusal with hosted labels, defeating G2 exactly as the integration test's own docstring warns, or theResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Division-by-zero and unconstrained busy_fraction with online=0 fleet | ModelCIRunnerFleetObservation permits online=0 (ge=0). The handler computes idle and busy_fraction from online; with online=0 the fraction is either a ZeroDivisionError inside a handler whose docstring asserts 'Nothing here raises', or is clamped by undocumented logic. Evidence.busy_fraction is constrained ge=0.0 but has no upper bound, so a busy value exceeding online would also validate silently. The visible handler excerpt truncates b
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Eager package init forces full dependency sync onto the critical path of every run | Both workflows now install the entire project dependency set (--frozen uv sync) before deciding, because importing anything under omnibase_infra executes an init that pulls database and broker surfaces. This converts a millisecond decision into a dependency-resolution step sitting ahead of every heavy job in every consuming workflow, and it is the stated reason the route job timeout was widened from 2 to 5 minutes.
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Force override semantics untestable from visible evidence; invalid values degrade silently | The
forceinput is a free-form workflow_call string threaded through the environment into--force. ModelCIRunnerRouteRequest validates it as EnumCIRunnerRouteForce; a typo such as 'fleett' would raise a pydantic ValidationError during build_request, which the boundary's catch-all converts to a hosted fallback with a probe-error reason. An operator error is therefore indistinguishable from a probe failure in theResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
min_online_fraction of 0.67 leaves no tolerance narrative for a fleet declared at 1-2 runners | The degraded floor is a fraction of fleet_expected_count. At small declared inventories (1 or 2 runners), 0.67 means one offline runner refuses the fleet on every run, recreating precisely the near-miss the contract describes for the old literal of 60 against a 60-runner cap. The contract documents the resize motivation but not the small-fleet behavior, and no test excerpt exercises fleet_expected_count below a h
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
Findings demoted from threads (anchor rejected)
-
[MAJOR] hostile-reviewer (glm-review)
Refusal visibility guarantee depends on contract data that is a plain unvalidated string tuple | capacity_downgrade_reasons is typed tuple[str, ...] with no cross-check against EnumCIRunnerRouteReason at the model level. The added contract test claims to bind the contract to the enum, which is good, but the model itself accepts any strings; a future contract edit misspelling a reason stops reversal for that class with no load-time failure, and the private-repository hosted-refusal guarantee then depends on
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Route job timeout widened on the critical path of every consuming run | The timeout increase from 2 to 5 minutes plus a full dependency sync adds latency to every run in the repository even when the routing decision itself is inert (the seam still returns hosted verbatim). The comment argues the bound is tight; the counterargument is that a mechanism that is currently byte-identically inert need not be on the critical path at all. | Evidence: timeout-minutes: 5 ... This job sits on the critical path of ever
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Wiring test uses substring matching on shell body after criticizing exactly that technique | test_both_route_call_sites_set_up_the_environment_before_deciding asserts 'uv run --frozen' appears in the step's run body, and the contract test file explicitly argues text matching is the wrong tool because it fires on prose. The step body is not prose, but a reordering such as
uv run --frozen --no-progress python3or a refactor into a composite action silently breaks or bypasses the assertion without any behaviResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
decided_at serialized as untyped ISO string | ModelCIRunnerRouteDecision.decided_at is a plain str described as ISO-8601 UTC. Nothing constrains its format or timezone; the handler supplies datetime.now(UTC).isoformat(), but a replay or a future caller can write any string and the 'replayed without a network' claim loses its audit value. A datetime-typed field with pydantic serialization would enforce the format. | Evidence: decided_at: str = Field(description="ISO-8601 UTC timestamp of the decision.") | Fi
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
…ntract The committed baseline records the live dispatch corpus, so adding a contract makes it stale by construction. Regenerated with the harness rather than hand-edited. The diff is the whole evidence that this node adds no dispatch surface: the contract count moves 143 to 144 and nothing else changes. No new route, no new subscribe topic, no new dispatcher -- which is what a pure compute node with no event_bus block should look like, and would not be true if the node had acquired one.
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: glm-review
Models failed: codex
New finding threads: 3
Deduped (already posted on this PR): 0
Nit-level findings suppressed: 1
The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.
Findings not anchored to a changed file
-
[CRITICAL] hostile-reviewer (glm-review)
BLOCKED decision breaks the documented decision output contract | The reusable workflow declares its
decisionoutput ashosted | self_hosted, and the audit, saturation monitor, and every consuming workflow parse that field against a two-value grammar. The new handler can returnblockedwith an emptyruns_on. No truncated portion of the diff shows the workflow or audit being extended to handle a third value; the audit regex change only concerns consumer detection, not decision values. A refusal willResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Refusal exit code vs fail-closed floor interaction unverified in the workflow | The handler returns BLOCKED with empty runs_on and the boundary exits non-zero for a refusal, while the workflow shell contains a fail-closed floor that writes a usable runs-on (hosted) whenever GITHUB_OUTPUT lacks content. If the floor does not special-case exit 3, a private-repository refusal is silently converted into a hosted placement, the exact placement the operator ruling forbids. The integration test acknowledges this h
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Invalid force input degrades through the floor to hosted, defeating the private-repo ruling | The
forceworkflow_call input is a free-form string passed from the caller into--force "${FORCE}". If an invalid value (typo, injection of unexpected text, non-enum) causes the CLI or argparse to exit non-zero before a decision is recorded, the fail-closed floor emits hosted labels. On a private repository that is precisely the placement the ruling forbids, reached through a one-character typo rather than an eResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (glm-review)
Eager package init remains the root cause and is worked around, not fixed | The diff documents twice that importing anything under omnibase_infra executes the package init and pulls database and broker surfaces, and then pays for a full frozen dependency install on the critical path of every consuming workflow rather than making the node package importable in isolation. The new node is placed under the same omnibase_infra.nodes tree, so it inherits the defect it documents. Every future CI job that i
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Environment sync added to both route jobs on the critical path | Both the reusable route job and the probe job now perform a full
uv sync --frozen(even if cached, cache restore plus venv linking) ahead of a decision that takes milliseconds, on the critical path of every consuming run. The timeout was raised from 2 to 5 minutes to accommodate it. The probe job in particular runs the decision twice per repository per push and previously needed only a stdlib python3. | Evidence: "timeout-minutes: 5" with coResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Dual outputs labels and runs_on invite consumer divergence | The reusable workflow now emits the same label set under two output names and the audit deliberately accepts either. Two wire names for one value is a second copy of an interface; a future change that populates one and not the other (or formats them differently) will be invisible to half the consumers, the same drift class the PR's own prose warns about elsewhere. | Evidence: "runs_on:
... value: ${{ jobs.route.outputs.runs_on }}" alongsidResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
Findings demoted from threads (anchor rejected)
-
[MINOR] hostile-reviewer (glm-review)
Audit regex over-matches unrelated output reads | ROUTE_CONSUMER_RE now treats any
needs.<job>.outputs.labelsoroutputs.runs_onas evidence of a routed job. Any job in the repository that reads an output literally named labels or runs_on from a non-routing job will be classified as routed, suppressing the audit's unrouted-job report. The regex is not anchored to a route job identity. | Evidence: ROUTE_CONSUMER_RE = re.compile(r"needs.([A-Za-z0-9_-]+).outputs.(?:labels|runs_on)") | Fix: Cross-referenResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
Private-repo forced-fleet semantics untested and undocumented against the ruling | The force documentation states FLEET cannot widen past the seam and cannot take a fork PR, and HOSTED is refused for a private repository, but says nothing about force=fleet on a private repository. A private repository running on self-hosted lab hardware is a different trust question the ruling does not obviously settle, and no visible test pins the behavior. | Evidence: "FLEET still cannot widen past the seam ceiling and st
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (glm-review)
min_online_fraction boundary and zero-online edge under-specified | The fraction floor is compared against fleet_expected_count from config/runner_fleet.yaml, but the truncated handler does not show whether comparison is inclusive, how online > expected_count (runners registered beyond inventory) is treated, or how the fraction interacts with online=0 (0/60 = 0, degraded; fine) versus expected_count read from a stale or edited inventory file. A fleet file that doubles the declared count halves the effective
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
…clared by its contract (#3604) * feat(OMN-18412): the CI runner routing decision is a COMPUTE node, declared by its contract Operator direction, 2026-09-15: contract-driven, OmniNode primitives, no one-offs. The per-run routing decision was a 900-line script with its thresholds in a config file. It is now node_ci_runner_route_compute: a contract, a declarative node, and one stateless definition-B handler taking a typed request and returning a typed decision. WHERE IT LIVES AND WHY. omnibase_infra owns the CI routing surface already -- the fleet inventory, the hosted-workflow list, the routing audit, the fleet canary and the summary gate are all here, and the repository holds 125 nodes including the compute archetypes. omnimarket owns the runtime-deployment domain, which is a different question from where a CI job executes. WHAT THE CONTRACT DECLARES: the label sets, every threshold, the reasons a hosted verdict may be reversed for a repository that may not run hosted, and the private-repository refusal. Parsed into a typed policy whose fields are all required, so a contract missing one fails at load naming it rather than at the first run that needed it. No threshold is read from an environment variable and none is restated in the old config file -- two copies are two policies that agree until someone edits one, and a test now fails if one reappears. THE FLEET SIZE IS READ FROM THE FLEET INVENTORY, not restated. That is not tidiness: the degraded floor was the literal 60, chosen as two thirds of the 88-runner fleet of the time, and the fleet was capped to 60 the same day this was written. The literal had become equal to the whole fleet, so one runner going offline would have refused the fleet on every run and reported it as an ordinary return to GitHub-hosted. As a fraction of the declared inventory the same two thirds tracks the cap. The script keeps exactly what a boundary should: the probes, request assembly and the CLI. It cannot drift from the decision because it no longer contains one. Its node imports are function-local by necessity -- the self-hosted lab-load probe imports this module on the fleet image's bare interpreter, which has neither PyYAML nor pydantic. An operator force input lands with it, subject to every safety rule rather than a bypass of them: fleet cannot widen past the seam ceiling or take a fork pull request, and hosted is still refused for a private repository. It exists so the kill switch a static seam value provided survives per-run routing. Not used: the sanctioned local node CLI. Measured today, it refuses to dispatch unless the workspace venv carries every sibling package installed from git, which on a hosted runner is minutes of install on the critical path of every run for a decision that takes milliseconds. The same handler executes either way; only the dispatch machinery differs. Recorded in the workflow beside the invocation rather than left as an unexplained choice. Proven, not asserted: 266 tests pass across the decision, the wiring, the incident replay, the routing audit and the new contract-binding suite. Four live dry runs through the node -- private plus saturation gives the fleet, public with identical inputs gives hosted as the control, public with an idle fleet and a fresh lab record gives the fleet, and the same run forced hosted gives hosted. The contract suite asserts the declared reversal reasons are real enum members and exclude every trust reason, which is the one defect nothing else can see: a misspelling there silently stops reversing. No Actions variable is written. Routing stays inert wherever a seam reads hosted. * test(OMN-18412): exercise the routing decision as CI invokes it The unit suites cover the handler and the contract suite covers the node's shape. Neither covers the seam that decides real placement: a subprocess reading the committed contract and fleet inventory off disk, writing the four lines a workflow consumes, and signalling a refusal through its exit status. That seam has broken twice in ways every unit test survived. A JSON seam value interpolated into a quoted shell argument lost its inner quotes and reached the module as non-JSON, so every decision came back unparseable and looked exactly like a correctly inert mechanism. And a refusal that exits non-zero is worthless if the workflow's fail-closed floor overwrites its labels first. Six cases, each against the real contract: the fleet path, saturation, the seam surviving the shell with an unparseable-seam positive control so the assertion discriminates, a private repository refused a hosted downgrade, a refusal that exits 3 and still leaves both a record and the labels line the floor checks for, and the operator override in both directions. * fix(OMN-18412): an expression in a workflow_call output description fails every caller at startup Measured on run 35037012235: ci.yml and the routing probe both concluded failure with ZERO jobs, and the run title fell back to the file path instead of the workflow name. That is the whole visible symptom. No check reports on a run that never started, so the pull request showed 62 green checks, no red one, and a merge state of BLOCKED with nothing to point at. The cause was an illustrative snippet I put in the new runs_on output's DESCRIPTION. Actions evaluates expressions inside a workflow_call outputs block, where the needs context does not exist, so the reusable failed to compile and took every calling workflow with it. The description is now prose. A test asserts no workflow_call input or output description in either routing workflow carries an expression, because this failure is invisible to every check-based gate by construction. * fix(OMN-18412): the route job must sync the environment, or it reports green while inert Measured on run 35039101488. The route job installed two pinned packages -- the models need only pydantic and a YAML parser -- and then failed with `No module named 'omnibase_core'`, fell through to its fail-closed floor, and reported SUCCESS carrying `reason=probe_error:module_unavailable`. Both consumer jobs scheduled hosted. Nothing was red. The mechanism was completely inert and the only evidence was a reason string in an artifact. The cause is not this node: importing anything under the package executes its __init__, which eagerly pulls the database and broker surfaces, so no node can be imported without the project's dependency set. Loading modules by file path around that would be bespoke plumbing for one job; syncing the environment with the repository's own shared setup action is what every other job here does. Both call sites now use that action and run the module inside the synced environment. The job's ceiling goes from 2 to 5 minutes -- the bound the wiring suite already codified -- rather than wider, because this job is on the critical path of every consuming run and a sync that stops fitting should fail rather than silently cost every pipeline minutes. A test asserts the setup step exists and precedes the decision at both call sites. That is the only durable guard: the floor exists precisely so a broken router still hands the run a usable runs-on, which is why nothing else can see this failure. * chore(OMN-18412): register the routing node's entry point A contract-backed node is discoverable through the onex.nodes entry points, and the entry-point checker holds the generated section to the set of node directories on disk. Adding the node without its entry point left that test red -- correctly: a node the runtime cannot resolve by name is a node only its own importer can reach. Generated with the repo's generator against this worktree rather than hand-edited, so the section stays byte-identical to what the checker regenerates. * chore(OMN-18412): refresh the dispatch-parity baseline for the new contract The committed baseline records the live dispatch corpus, so adding a contract makes it stale by construction. Regenerated with the harness rather than hand-edited. The diff is the whole evidence that this node adds no dispatch surface: the contract count moves 143 to 144 and nothing else changes. No new route, no new subscribe topic, no new dispatcher -- which is what a pure compute node with no event_bus block should look like, and would not be true if the node had acquired one.
#9745) * evidence: OCC companion pass 1 for OmniNode-ai/omnibase_infra#3604 * evidence: OCC companion self-bind for #9745 --------- Co-authored-by: node-occ-companion-effect <occ-companion-effect@omninode.ai>
OMN-18412
Operator direction, 2026-09-15: contract-driven, OmniNode primitives, no one-offs. The per-run CI routing decision was a 900-line script whose thresholds lived in a config file. It is now
node_ci_runner_route_compute: a contract, a declarative node, and one stateless definition-B handler taking a typed request and returning a typed decision.Where it lives, and why
omnibase_infra. It already owns every input and every consumer of this decision: the fleet inventory, the hosted-workflow list, the routing audit, the fleet canary and the CI Summary umbrella.omnimarketowns the runtime-deployment domain, which is a different question from where a CI job executes.The handler-ownership gate fired on that choice, and the allowlist entry is its intended path, not a bypass.
reject-node-implementations.shsays business-domain node handlers belong inomnimarketand that an infra-owned handler staying here must carry a justified allowlist entry. Placement of this repository's own CI jobs, decided from this repository's own fleet inventory, is infra-owned by the same reading that already keeps the broker disk-watermark probe here. The entry states that rather than repeating the generic line.What the contract declares
The label sets, every threshold, the reasons a hosted verdict may be reversed for a repository that may not run hosted, and the private-repository refusal. It parses into a typed policy whose fields are all required, so a contract missing one fails at load naming it. No threshold is read from an environment variable, and none is restated in the old config file. A test fails if one reappears there.
The fleet size is read from the inventory, and that caught a live near-miss
The degraded floor was the literal
60, chosen as roughly two thirds of the 88-runner fleet of the time. The fleet was capped to 60 runners today. The literal had silently become equal to the entire fleet, so one runner going offline would have refused the fleet on every run and reported it as an ordinary return to GitHub-hosted. Read as a fraction ofconfig/runner_fleet.yamlexpected_count, the same two thirds tracks the cap. A test asserts the floor leaves room for a runner to go offline.What the script keeps
Probes, request assembly, the CLI. It cannot drift from the decision because it no longer contains one. Its node imports are function-local by necessity: the self-hosted lab-load probe imports this module on the fleet image's bare interpreter, which has neither PyYAML nor pydantic, and ten of twelve scheduled probe runs once failed on exactly such a module-scope import.
The operator override
A
forceinput ofauto,fleetorhosted, subject to every safety rule rather than a bypass of them: fleet cannot widen past the seam ceiling or take a fork pull request, and hosted is still refused for a private repository. It exists so the kill switch a static seam value provided survives per-run routing.Not used: the sanctioned local node CLI, and the measurement why
onex node <name> --input <file>refuses to dispatch unless the workspace venv carries every sibling package installed from git. On a hosted runner that is minutes of install on the critical path of every consuming run, for a decision that takes milliseconds. The route job instead installs two pinned packages and imports the node's handler from the checked-out tree, so the decision executing in CI is the same code the tests bind to. Recorded in the workflow beside the invocation.Four live dry runs through the node, real repository visibility:
Tests: 266 pass across the decision suite, the wiring suite, the incident replay, the routing audit and the new contract-binding suite. The contract suite asserts the declared reversal reasons are real enum members and exclude every trust reason, which is the one defect nothing else can see: a misspelling there silently stops reversing and a private repository lands hosted.
Local:
ruff format,ruff check,mypyclean; architecture and pattern validators pass (the models were split one-per-file and one field renamed to satisfy them).Out of scope
No Actions variable is written. No seam is flipped. Routing stays inert wherever a seam resolves to hosted, which is every public repository today.
Evidence-Ticket: OMN-18412
Evidence-Source: OCC#9745