Skip to content

refactor(autoland): give each plan step type its own class - #69

Merged
micahjsmith merged 1 commit into
micahjsmith/stack/2from
micahjsmith/stack/3
Oct 12, 2026
Merged

micahjsmith merged 1 commit into
micahjsmith/stack/2from
micahjsmith/stack/3

Conversation

@micahjsmith

@micahjsmith micahjsmith commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Stacked PRs:


Summary

Phase 1 of the autoland step-abstraction stack. Plan steps were plain dataclasses joined by PlanStep = Union[LandStep, WorkflowStep, ConfirmStep], with isinstance chains in every module that handled a plan. This adds an AutolandStep base class and moves the per-type behavior for plan syntax, persistence, and replanning progress onto the step classes.

  • New stack_pr.autoland.steps package: base.py (the AutolandStep ABC, a PlanParser context holding the stack, pr_is_merged, the next positional index, and the current line number, and the step registry), plus land.py, workflow.py, and confirm.py. The step classes keep their names and fields and are re-exported from model.py. PlanStep is now an alias of AutolandStep.
  • Each step type declares kind (state-file tag), keyword (plan syntax), and label. @register_step adds it to a registry that is looked up by kind or keyword. Registering the same kind or keyword twice raises an error.
  • Persistence: to_dict() / from_dict() replace model._STEP_TYPES and the type switch in _deserialize_step. The state file is byte-for-byte unchanged (STATE_VERSION stays 1).
  • Plan syntax: parse(args, parser) and plan_line(stack) per step. parse_plan and format_plan_for_editor now dispatch through the registry. The comment-column rule is a per-step aligns_comments flag (false for confirm). A bare w still counts as an unrecognized step, via bare_keyword_ok. PR-reference/URL resolution moves into steps/land.py unchanged.
  • Progress: assume_completed(), is_checkpoint_done(), replan_key(landed_prs), credit_from(old), landed_pr(stack), and marks_landed_prefix replace plan._mark_skipped, _checkpoint_done, _credit, and the type switch in _checkpoint_keys. landed_prefix_end, is_assumed_completed, and carry_over_progress stay in plan.py and now call these methods.
  • engine.py, display.py, status.py, and native_stack.py keep their isinstance dispatch for now; later phases handle them. display.py and status.py only get assert isinstance(step, ConfirmStep) at their "the remaining type" fallthroughs, because mypy can no longer narrow a base class to the last member of the old union.

Behavior change: a state file step with an unknown type now fails to load with Unknown plan step type: '<type>'. Before, it was silently loaded as a ConfirmStep, which was a latent bug. --resume/--replan already report ValueErrors from loading as "Failed to load state file".

Test plan

  • Golden autoland scenarios (tests/test_autoland_golden.py) pass with no golden files regenerated, including the checkpoint JSON
  • New tests/test_autoland_steps.py: registry lookup by kind and keyword, duplicate kind/keyword registration error (with no partial registration), unknown state-file step type error, and a round trip of every registered step type through the state file and through plan text (format_plan_for_editor -> parse_plan)
  • Full suite passes with GH_CONFIG_DIR empty and GH_TOKEN unset

🤖 Generated with Claude Code

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

## Summary

Phase 1 of the autoland step-abstraction stack. Plan steps were plain dataclasses joined by `PlanStep = Union[LandStep, WorkflowStep, ConfirmStep]`, with `isinstance` chains in every module that handled a plan. This adds an `AutolandStep` base class and moves the per-type behavior for plan syntax, persistence, and replanning progress onto the step classes.

- New `stack_pr.autoland.steps` package: `base.py` (the `AutolandStep` ABC, a `PlanParser` context holding the stack, `pr_is_merged`, the next positional index, and the current line number, and the step registry), plus `land.py`, `workflow.py`, and `confirm.py`. The step classes keep their names and fields and are re-exported from `model.py`. `PlanStep` is now an alias of `AutolandStep`.
- Each step type declares `kind` (state-file tag), `keyword` (plan syntax), and `label`. `@register_step` adds it to a registry that is looked up by kind or keyword. Registering the same kind or keyword twice raises an error.
- Persistence: `to_dict()` / `from_dict()` replace `model._STEP_TYPES` and the type switch in `_deserialize_step`. The state file is byte-for-byte unchanged (`STATE_VERSION` stays 1).
- Plan syntax: `parse(args, parser)` and `plan_line(stack)` per step. `parse_plan` and `format_plan_for_editor` now dispatch through the registry. The comment-column rule is a per-step `aligns_comments` flag (false for confirm). A bare `w` still counts as an unrecognized step, via `bare_keyword_ok`. PR-reference/URL resolution moves into `steps/land.py` unchanged.
- Progress: `assume_completed()`, `is_checkpoint_done()`, `replan_key(landed_prs)`, `credit_from(old)`, `landed_pr(stack)`, and `marks_landed_prefix` replace `plan._mark_skipped`, `_checkpoint_done`, `_credit`, and the type switch in `_checkpoint_keys`. `landed_prefix_end`, `is_assumed_completed`, and `carry_over_progress` stay in `plan.py` and now call these methods.
- `engine.py`, `display.py`, `status.py`, and `native_stack.py` keep their `isinstance` dispatch for now; later phases handle them. `display.py` and `status.py` only get `assert isinstance(step, ConfirmStep)` at their "the remaining type" fallthroughs, because mypy can no longer narrow a base class to the last member of the old union.

**Behavior change:** a state file step with an unknown `type` now fails to load with `Unknown plan step type: '<type>'`. Before, it was silently loaded as a `ConfirmStep`, which was a latent bug. `--resume`/`--replan` already report `ValueError`s from loading as "Failed to load state file".

## Test plan

- [x] Golden autoland scenarios (`tests/test_autoland_golden.py`) pass with no golden files regenerated, including the checkpoint JSON
- [x] New `tests/test_autoland_steps.py`: registry lookup by kind and keyword, duplicate kind/keyword registration error (with no partial registration), unknown state-file step type error, and a round trip of every registered step type through the state file and through plan text (`format_plan_for_editor` -> `parse_plan`)
- [x] Full suite passes with `GH_CONFIG_DIR` empty 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: #69, branch: micahjsmith/stack/3
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