Skip to content

refactor(autoland): merge land runs as a stack through a plan optimization policy - #72

Merged
micahjsmith merged 1 commit into
micahjsmith/stack/5from
micahjsmith/stack/6
Oct 12, 2026
Merged

micahjsmith merged 1 commit into
micahjsmith/stack/5from
micahjsmith/stack/6

Conversation

@micahjsmith

@micahjsmith micahjsmith commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

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 of ExecutionUnits 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

  • The golden autoland scenarios (tests/test_autoland_golden.py, including merge_as_stack and stack_merge_falls_through) pass with no golden files regenerated.
  • 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 StepUnits; 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.
  • The full suite passes with an empty GH_CONFIG_DIR and GH_TOKEN unset.

🤖 Generated with Claude Code

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

…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
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