Skip to content

feat(OMN-18412): the CI runner routing decision is a COMPUTE node, declared by its contract - #3604

Merged
jonahgabriel merged 6 commits into
devfrom
jonah/omn-18412-ci-runner-route-compute-node
Sep 16, 2026
Merged

jonahgabriel merged 6 commits into
devfrom
jonah/omn-18412-ci-runner-route-compute-node

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

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. omnimarket owns 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.sh says business-domain node handlers belong in omnimarket and 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 of config/runner_fleet.yaml expected_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 force input of auto, fleet or hosted, 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:

private repo + saturated fleet + fleet ceiling  -> ["self-hosted","omnibase-ci"]  private_repo_no_hosted_downgrade
public repo  + identical inputs                 -> ["ubuntu-latest"]             fleet_saturated        (control)
public repo  + idle fleet + fresh lab record    -> ["self-hosted","omnibase-ci"]  capacity_available
public repo  + same inputs + force hosted       -> ["ubuntu-latest"]             forced_hosted

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, mypy clean; 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

…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.
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Hostile Reviewer — DEGRADED (informational)

Critical findings: 1
Major findings: 3
Total findings: 10
Models succeeded: glm-review

Note: Fewer than 2 reviewer models succeeded. Degraded results are informational (OMN-8468/OMN-8524) and do not block merge. Error: cli_review exit 2 (fewer than 2 models succeeded — partial/total outage)


Semantics (OMN-17492 — the model finds, thread resolution gates)

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)

@github-actions github-actions Bot 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.

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 Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MAJOR] hostile-reviewer (glm-review)

    force input accepted as free-form workflow_dispatch string | The new force workflow 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 FLE

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 Gate blocks 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 Gate blocks 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 Gate blocks 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 Gate blocks 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 Gate blocks 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 Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

@github-actions github-actions Bot 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.

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 Gate blocks 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, and set -uo pipefail (no -e), but no visible use of rc in the step. If rc is 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 Gate blocks 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__.py and the enums module commonly carry si

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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=88 as a literal, and test_runner_route_decision.py sets FLEET_EXPECTED = 60, none of them reading the com

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 co

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 15, 2026
#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.

@github-actions github-actions Bot 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.

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 Gate blocks 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 Gate blocks 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 Gate blocks 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 Gate blocks 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 json inside 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 bound

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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: string with no choice restriction. 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 Action

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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.

@github-actions github-actions Bot 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.

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 t

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 force workflow_call input is type: string with 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 forc

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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-rou

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 (--frozen install 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 is

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 Gate blocks 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.

@github-actions github-actions Bot 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.

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 the

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 Gate blocks 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 force input 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 the

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 Gate blocks 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 Gate blocks 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 python3 or a refactor into a composite action silently breaks or bypasses the assertion without any behavi

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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.

@github-actions github-actions Bot 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.

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 decision output as hosted | self_hosted, and the audit, saturation monitor, and every consuming workflow parse that field against a two-value grammar. The new handler can return blocked with an empty runs_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 will

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 force workflow_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 e

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 co

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 }}" alongsid

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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.labels or outputs.runs_on as 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-referen

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks 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 Gate blocks 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 Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

@jonahgabriel
jonahgabriel merged commit 341ac00 into dev Sep 16, 2026
207 of 211 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-18412-ci-runner-route-compute-node branch September 16, 2026 01:34
Patel230 pushed a commit that referenced this pull request Sep 16, 2026
…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.
Patel230 pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 16, 2026
#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>
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.

1 participant