Repository navigation
refactor(autoland): merge land runs as a stack through a plan optimization policy - #72
Merged
Merged
Conversation
…ation policy ## Summary Phase 4 of the autoland step-abstraction stack. After the plan is parsed, the engine runs a plan optimization pass, driven by a registry of policies, and executes the result. Native stack merging moves out of the engine into the first such policy, so the base steps are fully independent of each other and of the engine. - **Execution units** (new `autoland/optimize/` package). The engine executes a list of `ExecutionUnit`s covering the rest of the plan, from `current_step`. A unit covers one or more consecutive plan step indices (`indices: range`) and has `execute(uc: UnitContext) -> str` (the abort reason, or "") and `preview() -> list[str]`. The default `StepUnit` wraps one step and just executes it. - **`UnitContext`** is what a unit runs its steps with: `run`, `opts`, `common`, `services`, and three engine callbacks: - `enter(index)`: make the step current and checkpoint, exactly as the engine loop did before each step. - `execute_step(index)`: build the step's `StepContext`, execute it, and record the landed SHA it reports. - `checkpoint()`: save the run. So a composite unit still checkpoints and executes each of its steps exactly as a `StepUnit` would. - **Optimization policies.** An `OptimizationPolicy` is a `Protocol` with a `name`, `applies(opts) -> bool`, and `optimize(units, run, opts) -> units`. `@register_policy` adds a policy class to the ordered `POLICIES` registry. A duplicate name is an error. `optimize_plan(run, opts, policies=None)` starts from one `StepUnit` per remaining step, applies each applicable policy in registration order, and raises if a policy's result doesn't cover the remaining steps in order. To add a policy, write the class, decorate it, and import its module from `optimize/__init__.py`. `execute_plan` and `print_execution_preview` take an optional `policies` registry, which tests use to plug in their own. - **Policies hold the cross-step knowledge.** A policy may look at step types and at the run. The steps never know about each other or about policies. - **`NativeStackMergePolicy`** (`optimize/stack_merge.py`, on with `--merge-as-stack`) groups each run of two or more consecutive land steps whose PRs are still open into a `StackMergeUnit`. Any other step ends a run: a `w` or `c` step, an already-landed `l`, or a merged PR. A run of one is left alone. The unit performs the stack merge with `native_stack.land_as_native_stack` and then executes each member step itself, so per-step output, skips, and landed-SHA reporting are unchanged. If the merge falls through, the steps it covered land one at a time without retrying it, the same semantics as the old `unstacked_through`. - **Re-validation at execution time.** Before each of its steps (until a merge falls through), a `StackMergeUnit` recomputes, from the live stack, which of its following steps still land an open PR, and only stack-merges those if there are at least two. This matches today's behavior, where the run was evaluated when its first step was reached: a PR merged elsewhere in the meantime shortens the run. - **The optimized plan is never persisted.** It exists only for one execution. The state file still holds the user's plan, byte-identical, and `current_step` is still an index into it. Every fresh, resumed, or replanned run optimizes again from `current_step`. - **The preview derives from the units.** `print_native_stack_runs` is replaced by `engine.print_execution_preview`, which prints each unit's `preview()` lines from the same `optimize_plan` that `execute_plan` uses, so the preview and the execution can't disagree. The text is unchanged. `commands.py` calls it in the same three places. - The temporary `_NativeStackMerge.before_step` hook is gone from the engine. `native_stack.py` keeps GitHub's side of a stack merge (`land_as_native_stack` and its helpers). The run detection that peeked at neighboring steps (`native_stack_run`, `native_stack_runs`) moves into the policy. No user-visible change. Console output, checkpoints, and status output are byte-for-byte identical. ## Test plan - [x] The golden autoland scenarios (`tests/test_autoland_golden.py`, including `merge_as_stack` and `stack_merge_falls_through`) pass with no golden files regenerated. - [x] New `tests/test_autoland_optimize.py`: - Registry: policies run in registration order; registering a name twice is an error; a policy that drops a step is rejected. - Native-stack policy grouping: runs split by `w`/`c` steps (moved from `test_autoland.py`); already-landed steps and merged PRs left out of runs; runs of one left as `StepUnit`s; runs start at `current_step`; no grouping with `merge_as_stack` off; the preview text for each run. - `StackMergeUnit` execution: a stack merge followed by each step, with each step entered (checkpointed as current) before its work; a fall-through that lands the rest one at a time without retrying; a stack-merge abort stopping before the step; a step abort stopping the unit. - Re-validation: a PR merged after optimization shortens the merged run; a run shortened to one PR is not stack-merged. - Extension point: a test-only policy, registered from the test into its own registry, reshapes both the preview and the execution through `execute_plan`, with one checkpoint per step. - [x] The full suite passes with an empty `GH_CONFIG_DIR` and `GH_TOKEN` unset. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> stack-info: PR: #72, branch: micahjsmith/stack/6
micahjsmith
force-pushed
the
micahjsmith/stack/6
branch
from
October 12, 2026 23:05
1dbb498 to
8297100
Compare
This was referenced Oct 12, 2026
micahjsmith
added this pull request to stack #77
October 12, 2026 23:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PRs:
Summary
Phase 4 of the autoland step-abstraction stack. After the plan is parsed, the engine runs a plan optimization pass, driven by a registry of policies, and executes the result. Native stack merging moves out of the engine into the first such policy, so the base steps are fully independent of each other and of the engine.
Execution units (new
autoland/optimize/package). The engine executes a list ofExecutionUnits covering the rest of the plan, fromcurrent_step. A unit covers one or more consecutive plan step indices (indices: range) and hasexecute(uc: UnitContext) -> str(the abort reason, or "") andpreview() -> list[str]. The defaultStepUnitwraps one step and just executes it.UnitContextis what a unit runs its steps with:run,opts,common,services, and three engine callbacks:enter(index): make the step current and checkpoint, exactly as the engine loop did before each step.execute_step(index): build the step'sStepContext, execute it, and record the landed SHA it reports.checkpoint(): save the run.So a composite unit still checkpoints and executes each of its steps exactly as a
StepUnitwould.Optimization policies. An
OptimizationPolicyis aProtocolwith aname,applies(opts) -> bool, andoptimize(units, run, opts) -> units.@register_policyadds a policy class to the orderedPOLICIESregistry. A duplicate name is an error.optimize_plan(run, opts, policies=None)starts from oneStepUnitper remaining step, applies each applicable policy in registration order, and raises if a policy's result doesn't cover the remaining steps in order. To add a policy, write the class, decorate it, and import its module fromoptimize/__init__.py.execute_planandprint_execution_previewtake an optionalpoliciesregistry, which tests use to plug in their own.Policies hold the cross-step knowledge. A policy may look at step types and at the run. The steps never know about each other or about policies.
NativeStackMergePolicy(optimize/stack_merge.py, on with--merge-as-stack) groups each run of two or more consecutive land steps whose PRs are still open into aStackMergeUnit. Any other step ends a run: aworcstep, an already-landedl, or a merged PR. A run of one is left alone. The unit performs the stack merge withnative_stack.land_as_native_stackand then executes each member step itself, so per-step output, skips, and landed-SHA reporting are unchanged. If the merge falls through, the steps it covered land one at a time without retrying it, the same semantics as the oldunstacked_through.Re-validation at execution time. Before each of its steps (until a merge falls through), a
StackMergeUnitrecomputes, from the live stack, which of its following steps still land an open PR, and only stack-merges those if there are at least two. This matches today's behavior, where the run was evaluated when its first step was reached: a PR merged elsewhere in the meantime shortens the run.The optimized plan is never persisted. It exists only for one execution. The state file still holds the user's plan, byte-identical, and
current_stepis still an index into it. Every fresh, resumed, or replanned run optimizes again fromcurrent_step.The preview derives from the units.
print_native_stack_runsis replaced byengine.print_execution_preview, which prints each unit'spreview()lines from the sameoptimize_planthatexecute_planuses, so the preview and the execution can't disagree. The text is unchanged.commands.pycalls it in the same three places.The temporary
_NativeStackMerge.before_stephook is gone from the engine.native_stack.pykeeps GitHub's side of a stack merge (land_as_native_stackand its helpers). The run detection that peeked at neighboring steps (native_stack_run,native_stack_runs) moves into the policy.No user-visible change. Console output, checkpoints, and status output are byte-for-byte identical.
Test plan
tests/test_autoland_golden.py, includingmerge_as_stackandstack_merge_falls_through) pass with no golden files regenerated.tests/test_autoland_optimize.py:w/csteps (moved fromtest_autoland.py); already-landed steps and merged PRs left out of runs; runs of one left asStepUnits; runs start atcurrent_step; no grouping withmerge_as_stackoff; the preview text for each run.StackMergeUnitexecution: a stack merge followed by each step, with each step entered (checkpointed as current) before its work; a fall-through that lands the rest one at a time without retrying; a stack-merge abort stopping before the step; a step abort stopping the unit.execute_plan, with one checkpoint per step.GH_CONFIG_DIRandGH_TOKENunset.🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com