Skip to content

Relocate the enablement subsystem and make ENABLEMENT the sixth phase - #1491

Open
ZhengGong-amd wants to merge 17 commits into
mainfrom
feat/zgong/enablement-phase
Open

Relocate the enablement subsystem and make ENABLEMENT the sixth phase#1491
ZhengGong-amd wants to merge 17 commits into
mainfrom
feat/zgong/enablement-phase

Conversation

@ZhengGong-amd

Copy link
Copy Markdown
Collaborator

Description: what and why

Two related changes to the enablement subsystem, landed together because the second depends on the module layout the first establishes.

Part 1 — module relocation (commits 1–8). Enablement code lived in six places across agents/ and orchestrator/. This collapses it to three owners, deletes the repo's only reverse dependency edge, and moves 26 test files next to the code they exercise.

The reverse edge is the substantive part: agents/framework/enablement_ops.py reached back into orchestrator.framework.paths through a function-local import wrapped in a bare except Exception: pass that existed only to hide the cycle. As orchestrator/enablement/mandate.py that becomes an ordinary relative import and the guard disappears. Likewise classify_failure had no consumer inside the agent runtime — all 22 call sites were in the orchestrator — so living in agents/ made the orchestrator import agent Python in-process and rendered the subprocess JSON contract decorative. It moves to common/failure_signature.py.

Two silent failures fixed in passing: build_lifecycle._driver_command spawned python -m against a hardcoded module path (now derived from targeted_build.__name__), and test_git_foreign_checkout_callers was scanning a 33-line shim instead of the module it meant to guard.

Part 2 — ENABLEMENT as the sixth phase (commits 9–17). The chain is now:

PRELUDE → ENABLEMENT → FRAMEWORK_AGENT → KERNEL_AGENT → SWEEP → CLOSE

ENABLEMENT is entered from PRELUDE when the lane is admitted and baseline_failure_streak >= 1. A run whose baseline boots first try never enters it — streak == 0, so PRELUDE goes straight to _post_prelude_target().

compute_next_phase gains two keyword arguments (enablement_enabled, enablement_in_flight) supplied by the already-async _advance_phase_if_needed. machine_state.py gains no new first-party imports; the facts arrive through the same channel kernel_enabled and optimize_enabled already use.

The normal exit requires three conjuncts: baseline_tput > 0, not validation_pending, and no enablement work still in flight. The third is what makes the close-guard deletion sound — _maybe_rearm_authored_lane routes on the result's lane, not the current phase, so a build outliving its round would otherwise reopen validation_pending from inside FRAMEWORK_AGENT.

This is a net reduction in mechanism. enablement_close_guard_active(), MAX_SKIP_TO_CLOSE_SUPPRESSIONS, the skip_to_close_suppressions field, the intent-router suppression block, and the dead "enablement_stalled" stop reason are all deleted; the phase boundary now expresses what they approximated.

Budget: ENABLEMENT 5%, FRAMEWORK_AGENT 40→38%, KERNEL_AGENT 50→47%. Sum stays 1.0.

Follow-ups (commits 13–17) close what a branch-wide sweep turned up: a test left broken by a method rename, zero coverage on the new phase predicate, and EnablementBreakdown reaching the JSON but never the Markdown report.

Linked issue(s): close/fix refs

None.

Tests: added/updated? commands run?

Both. Added: five cases covering the ENABLEMENT entry and exit predicate (neither enablement_entered nor enablement_done was exercised by anything, and enablement_enabled=True had never been passed); test_enablement_relocation_completeness.py, a git ls-files sweep with a self-validating allowlist, an importability check for all ten canonical module paths, and a spawn-path check for _driver_command.

Fixed: test_environment_fault_round.py bound a lane helper by its pre-rename name, raising AttributeError at collection and silently dropping all 13 of its tests.

Updated: constant assertions (PHASE_NAMES, budget identity/sum, _PHASE_ORIENTATION, CLI redistribution figures, PHASE_GOAL_BLOCKS); three three-strike tests now set enablement_mode="off" to stay honest about the fast-fail path they assert. Six close-guard and suppression-counter tests deleted along with the mechanism.

pytest src/hyperloom/inference_optimizer/tests/test_phase_state_machine.py \
       src/hyperloom/inference_optimizer/tests/test_breakdown_report_integrity.py \
       src/hyperloom/inference_optimizer/tests/test_environment_fault_round.py \
       src/hyperloom/inference_optimizer/tests/test_sbd_optimizations.py
ruff check . && ruff format --check .

155 passed locally on the touched surfaces; leaving the full suite to CI.

Breaking changes: yes/no (details if yes)

No, for anything outside this repo. Internally, resume across the change is not supported: a session whose state.json records phase: "ENABLEMENT" cannot be resumed on pre-merge code. New sessions are unaffected, and EnablementRound.from_dict filters unknown keys, so older state.json files load without migration.

PR addresses single concern: yes/no (details if no)

No — two. The phase insertion needs the relocated modules to avoid re-creating the import cycle it deletes, so splitting them would mean landing the relocation, then immediately reopening the same files. They are cleanly separated in history (commits 1–8 and 9–17) and can be reviewed in that order.

Root cause is upstream (Magpie/TraceLens/GEAK/IntelliKit/AgentKernelArena), ticket filed:

Not upstream — entirely in-repo.

ZhengGong-amd and others added 17 commits September 10, 2026 10:14
The two report sub-trees were spelled by hand in five places. reports/bringup/
had no owner at all: bringup/persist.py and bringup/trees.py each appended the
literal to reports_dir() independently, and breakdown/collectors/sessions.py
bypassed enablement_dir() to spell the enablement setting-script path twice.

Adds bringup_dir(), enablement_builds_dir() and enablement_stacks_dir()
alongside the existing enablement_dir(), and exports BRINGUP_SEGMENT /
ENABLEMENT_SEGMENT so session_package.py's glob patterns -- which cannot take a
Path -- derive from the same source rather than repeating the string.

enablement_stacks_dir() covers <session>/enablement/stacks/, a second root
outside reports/ that nothing owned either.

Co-authored-by: Cursor <cursoragent@cursor.com>
The classifier had no production consumer inside the agent runtime -- all of
its call sites are in the orchestrator, two of them (bringup/ladder.py and
framework/adapters.py) at module level. Living under agents/framework/ meant
the orchestrator imported an agent's Python in-process, which makes the
subprocess JSON contract that is supposed to be the API surface decorative.

common/ is the only non-inverting home: placing it under enablement/ would
force bringup and framework to import enablement, which imports both of them.
The module has zero first-party imports, so it satisfies the common/ layering
guard as-is, and FailureSignature belongs to the same vocabulary as the
BootObservation and LadderStage already in common/bringup.py.

All 15 failure-kind string values are unchanged -- they are persisted in
session breakdowns.

Co-authored-by: Cursor <cursoragent@cursor.com>
enablement_ops.py held the repo's only reverse dependency edge: a function-local
import of orchestrator.framework.paths wrapped in a bare `except Exception: pass`,
both of which existed only to hide the cycle
orchestrator -> agents.framework.enablement_ops -> orchestrator.framework.paths.
As enablement/mandate.py that becomes an ordinary top-level import and the guard
goes with it. The narrower try/except around resolve_kernel_search_roots stays:
the probe itself can fail on a host with no source trees, which is a different
failure from the module being unimportable.

_enablement_artifacts.py was a private module of phases/ whose only production
consumer was the enablement lane. It becomes enablement/artifacts.py and loses
the underscore. This also removes the enablement -> phases module-level edge.

Both modules keep their module-level imports of agents.framework.keywords and
repo_map. That is the legal orchestrator -> agents direction, and keywords.py is
genuinely shared agent-side, so it must not move.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sixteen lines re-exporting one function and one constant. repo_url_for_framework
now comes straight from agents.framework.repo_map, which is what
enablement/params.py already did.

DISCOVER_FAILURE_RETRY_LIMIT moves to phases/framework.py rather than to
framework/artifacts.py: artifacts.py classifies candidate outcomes, and a
discovery retry bound has nothing to do with that. Its only two production
readers are in phases/framework.py, so it lands beside them and the module count
drops by one instead of staying flat.

Co-authored-by: Cursor <cursoragent@cursor.com>
bringup/argv_preflight.py reached framework/adapters.get_adapter() through a
function-local import purely to call argv_parser_source(), which pinned the whole
adapter registry -- venv creation, pip installs, ROCm probes -- at the bringup
layer. That is what blocked the acquisition half from moving.

The coupling turned out to be thinner than the class hierarchy suggests:
argv_parser_source has exactly one caller in the repo and all four
implementations are a bare `return "<source string>"` with no calls into the
acquisition side. Extracting the strings into framework/adapter_parsers.py lets
bringup import a 65-line lookup and nothing else, so no class hierarchy is split
and no shared base module is needed.

With that edge gone, adapters, stack_actions, localization, build_actions,
build_utils and targeted_build move to enablement/runtime/ intact. The cluster
depends on nothing in orchestrator/ outside itself, so the move adds no reverse
edges. framework/ is left holding paths.py, artifacts.py and adapter_parsers.py.

Two things that would have failed silently:

- build_lifecycle._driver_command spawned `python -m
  hyperloom.orchestrator.framework.targeted_build`. A stale string there raises
  at build-spawn time, not import time, so it now derives the path from the
  module's own __name__.
- test_git_foreign_checkout_callers reads a module's source by path to scan for
  unguarded git calls. Left pointing at the old path it would have scanned a
  33-line shim and silently protected nothing.

Also carries the attempt_root fix: enqueue_targeted_build pre-generates the
task_id so it can fill action.attempt_root before the row is written. The
executor's private _attempt_root() and the re-derive fallback in
enablement/build.py both go away, and the params no longer keep the enqueue-time
default they were documented as keeping.

Co-authored-by: Cursor <cursoragent@cursor.com>
Twenty-six test files under inference_optimizer/tests/ imported only
hyperloom.orchestrator.* and belonged in the packages they test. The style guide
already asks for **/tests/ next to the code under test, and pyproject's
src/**/tests glob collects the new directories with no config change.

The relocation is not a bare file move, because a test taken out of
inference_optimizer/tests/ silently loses that package's 466-line conftest --
including the autouse _isolate_session_layout_env, which clears the session-dir
pin and points MULTI_NODE_STATE_FILE at a missing sentinel. So:

- orchestrator/conftest.py takes _isolate_session_layout_env, launch_backend and
  virtual_clock. It sits at the orchestrator root rather than src/, whose scope
  would be every test package in the repo.
- orchestrator/tests/_helpers.py takes init_git_repo, git_commit_all,
  patch_integrate_patch_roots and variant_result. These were pulled in through
  `from .conftest import`, which pytest does not make available across packages,
  so they need a real importable module.

Every new tests/ directory gets an __init__.py. The repo is split on this today,
but with importmode=prepend two same-named files in two non-package directories
collide in sys.modules, and several of these basenames are generic enough to
collide later.

test_argv_refusal_round.py stays put: it imports fixtures from
test_bringup_round_scenario.py, which is one of four files that genuinely depend
on inference_optimizer.protocol / session / breakdown and belong where they are.

Co-authored-by: Cursor <cursoragent@cursor.com>
enablement/__init__.py was four lines with no __all__. It now exports the three
mandate names that callers outside the package address, and deliberately not the
four CoordinatorCollaborator subclasses: those are resolved by
Coordinator._COLLAB_MODULES through dotted strings, and exporting them would
invite instantiation outside the coordinator. This is the shape
bringup/__init__.py already uses.

The guard follows kernelforge's test_rename_completeness.py: a git ls-files
sweep, an allowlist whose every entry carries a written justification, and a
self-validating test that fails when an entry stops exempting anything. It
enforces two rules:

- No pre-relocation dotted path may come back. The relocation left no shims, so
  any such reference is a live regression rather than a deprecation.
- agents/framework/* must not import orchestrator/*. This is the rule that would
  have caught the enablement_ops reverse edge the day it landed. Scoping it to
  agents/framework rather than all of agents/ means it lands with an empty
  allowlist; the repo-wide version would have needed twenty entries, nearly all
  for agents/kernel/tools, and traded one real fix for a list nobody maintains.

It also asserts the relocated modules import and that _driver_command's spawn
argv still matches targeted_build's own __name__, since both are string-typed
and fail late.

adjustment.md records where the implementation departed from
enablement-refactor-2.plan.md and why.

Co-authored-by: Cursor <cursoragent@cursor.com>
Three fallbacks that could not fire. enqueue_targeted_build fills attempt_root,
so the re-derive in _route_succeeded_build and TargetedBuildExecutor._attempt_root
were unreachable; both are gone, and the enqueue now fills the field only when
the caller left it unset instead of overwriting an explicit one. The broad
`except Exception` around resolve_kernel_search_roots guarded nothing -- the
function returns an empty tuple for "nothing here to search" rather than raising
-- so it and the test that asserted the swallow are gone too.

Seven docstrings described the move rather than the code: "Moved from",
"Hoisted from", "previously reached these through". A reader of the module does
not need its history, and the style guide asks for neither.

orchestrator/conftest.py had copied four definitions that still existed in
inference_optimizer/tests/conftest.py. They move to
orchestrator/tests/_fixtures.py and both conftests import them, which is what
registers a fixture; neither package sits under a shared ancestor conftest, and
one at src/ would scope them to the whole repo.

The relocation guard shrinks: the empty agents/framework allowlist drove a loop
that could only ever match nothing, and two exemptions covered docstring lines
this commit deletes. adjustment.md keeps the decisions and drops the plan-step
narration.

Also repoints a comment in common/provenance.py that named targeted_build by its
old path.

Co-authored-by: Cursor <cursoragent@cursor.com>
The phase chain is now:
  PRELUDE -> ENABLEMENT -> FRAMEWORK_AGENT -> KERNEL_AGENT -> SWEEP -> CLOSE

ENABLEMENT is entered from PRELUDE when enablement is admitted
(--enablement != off, not multi-node) and at least one baseline has failed
(baseline_failure_streak >= 1).  A healthy run never enters the phase.

compute_next_phase gains three keyword arguments -- enablement_enabled,
enablement_stalled (from RoundStore.consecutive_stalled), and
enablement_in_flight -- passed by the already-async _advance_phase_if_needed.
machine_state.py acquires no new first-party imports.

Normal exit requires all three: baseline_tput > 0, no open revalidation
window, and no queued/running enablement targeted_build or integrate_patch.
The third conjunct is what makes the close-guard collapse sound: without it
a build outliving the round would re-open validation_pending from inside
FRAMEWORK_AGENT through _maybe_rearm_authored_lane.

Terminal exits: server_argv_invalid, environment_fault (written by the lane's
terminal helpers via stop_reason, routed by _global_terminal), and
enablement_attempts_exhausted (consecutive_stalled >= ENABLEMENT_MAX_ATTEMPTS).

The close guard is deleted:
- enablement_close_guard_active() from shared_state.py
- MAX_SKIP_TO_CLOSE_SUPPRESSIONS constant
- skip_to_close_suppressions field from EnablementRound
- the suppression block and observation in intent_router.py
The drain exit condition makes validation_pending implies phase == ENABLEMENT
a consequence of the machine, not of a separate predicate.

Both the three-strike gate (baseline_failure_streak >= 3) and the combined
backstop (_BASELINE_MAX_TOTAL_FAILURES = 3) in writeback.py are suppressed
while phase == ENABLEMENT. Without this, the cap of 8 rounds is unreachable
because 3 failures terminate the run first.

The dead stop reason "enablement_stalled" is removed from STOP_REASON_VOCAB
(it had no production writer). ENABLEMENT_MAX_ATTEMPTS moves from
coordinator.py to machine_state.py, removing the import inversion through
lane.py.

Budget: ENABLEMENT 5%, FRAMEWORK_AGENT down from 40% to 38%,
KERNEL_AGENT down from 50% to 47%.  Sum stays 1.0; work >= 0.8.

Allowlist: PRELUDE's set plus specialist, integrate_patch and targeted_build.
targeted_build is added because cancel_queued_not_allowed fires on every
transition and targeted_build was in no allowlist; it stays non-proposable
because allowed_actions_for() subtracts COORDINATOR_INTERNAL_ACTIONS.

Surfaces updated: _PHASE_ORIENTATION (critic gets an ENABLEMENT entry),
_BASELINE_RECOVERY_PHASES (rules F1/F2 render in ENABLEMENT too),
orchestration.md (phase-goal section, roofline tag), v6.py phase_map,
attribution.py phase_buckets, render.py cycle_reloop set, CLI flag pair,
failure_recovery.md reference tag, and all external docs.

Three three-strike tests set enablement_mode="off" to keep exercising the
documented fast-fail path.  Six close-guard and suppression-counter tests
are deleted; one is rewritten against the phase check the machine now owns.

The SVG diagram is re-laid-out with a sixth box (dashed orange border for
ENABLEMENT, indicating conditional entry); the PNG should be regenerated from
the SVG.

Co-authored-by: Cursor <cursoragent@cursor.com>
The ENABLEMENT branch of compute_next_phase re-derived three exits the lane
already writes to stop_reason, which _global_terminal routes ahead of every
phase branch — dead code, and a second place to keep the vocabulary in sync.
Drop the branch's terminal arm and the enablement_stalled argument that fed
it; the branch now decides only the normal exit.

Fold the rest of the duplication the phase change introduced:

- _enablement_work_in_flight re-implemented the lane's _enablement_in_flight
  and got the discriminator wrong: it looked for params["enablement"], which
  TargetedBuildAction.to_state never sets, so a running build read as drained.
  Call the lane's own query, and only from inside the phase, since it renews
  the round lease as a side effect.
- The admission predicate lived in both the lane and the phase machine. It
  belongs to the lane; the phase machine reaches it through the collaborator
  registry. Hoisting it also absorbs the is_multi_node check that sat halfway
  down the pump, past work a multi-node host cannot use.
- writeback derived in_enablement twice in one block; derive it once, next to
  the eval-suppression flag it is read beside.
- _on_enter_enablement only logged, duplicating the ENTER lifecycle event the
  dispatcher already emits for every phase.

enablement_stalled is no longer written by anything now that the cap stops
with enablement_attempts_exhausted, so drop its report explanation and point
the three tests that sampled it at the reason that is actually reachable.

Add the enablement key to the PhaseBreakdown TypedDict so the schema again
describes what the collector emits, and trim the comments that restated their
own code or narrated the move.

Co-authored-by: Cursor <cursoragent@cursor.com>
…rminal

The lane helper was renamed in a prior commit; seven call sites in
test_environment_fault_round.py still used the old name, causing an
AttributeError at module collection time that silently dropped all 13
tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Neither enablement_entered nor enablement_done appeared in any test;
enablement_enabled=True was never passed. Five focused cases now cover:
- PRELUDE with a baseline_failure_streak routes to ENABLEMENT
- the branch is skipped when enablement_enabled=False
- ENABLEMENT exits (enablement_done) once tput is set and work is drained
- the phase holds while work is in flight
- the phase holds while validation_pending is set

Co-authored-by: Cursor <cursoragent@cursor.com>
EnablementBreakdown reached the JSON output but was invisible in the
Markdown report. The new renderer surfaces admission status, round
outcomes, a bounded rounds table, and a build-attempts table. Skipped
automatically when the enablement section is empty, so sessions that
never triggered enablement are unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>
- redistribute_budget_pct docstring: ENABLEMENT is excluded from
  absorbers alongside PRELUDE and CLOSE
- _post_prelude_target docstring: called on ENABLEMENT exit too, not
  only PRELUDE
- backfill_langfuse.py comment: add ENABLEMENT to the phase-span list
- session-breakdown.md: note that a subset renders in the Markdown report

Co-authored-by: Cursor <cursoragent@cursor.com>
Rasterized with resvg-py at 2x zoom (2800x1040) to match the SVG's
aspect ratio. The prior PNG (1662x946) predated the ENABLEMENT node
addition to the SVG source.

Co-authored-by: Cursor <cursoragent@cursor.com>
…p commits

The renderer read four field names no producer emits (attempts/opened_at
on rounds, status/framework on builds), so half of every table rendered
blank. Point the columns at the fields EnablementRoundSummary and
TargetedBuildAttemptSummary actually carry.

Also drop what was redundant:
- unused warnings list threaded into RenderedSection
- two near-identical table helpers collapsed into one
- per-row isinstance skips, which silently dropped malformed rows that
  render_section already reports
- a 10-row display cap duplicating the collector's _MAX_ROUNDS bound
- str()/or-None coercions md_kv_list and _md_cell already handle
- duplicated test state builders merged; a disjunctive assertion made
  exact and its explanatory comment removed

Restore the return-value precedence dropped from the _post_prelude_target
docstring and trim the session-breakdown note.

Co-authored-by: Cursor <cursoragent@cursor.com>
.gitignore:70 lists pr.md; it is a local scratch file for authoring the
PR body, not a repo artifact. Force-adding it in an earlier commit was a
mistake.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ZhengGong-amd
ZhengGong-amd requested a review from a team as a code owner September 11, 2026 09:09
Comment on lines +17 to +22
from hyperloom.orchestrator.tests._fixtures import ( # noqa: F401
NoLaunchBackendInstalled,
_isolate_session_layout_env,
launch_backend,
virtual_clock,
)
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