Skip to content

refactor(autoland): have steps describe themselves with a StepView - #70

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

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

Conversation

@micahjsmith

@micahjsmith micahjsmith commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Stacked PRs:


Summary

Phase 2 of the autoland step-abstraction stack. Each step type now says what it has to show, and display.py decides how it looks. Before, display.py and status.py switched on the step's type to pick its status label, color, subject, detail line, prose description, and JSON fields.

  • New steps/view.py holds the presentation-neutral contract. StepView is a frozen dataclass with these fields: outcome (a StepOutcome: done / active / pending / failed, moved from display's _Outcome with the same values), status ("Waiting for checks", "Assumed completed", ...), tone, key ("Don't generate table of contents for stacks with just 1 PR. modular/stack-pr#103", "deploy.yaml", the condition or "(no condition)"), description (the one-line prose used in "Next steps" and in replan warnings), note (the PR title), detail (a tuple of fragments for the second line), and data (the type-specific --status -o json fields).
  • tone is a semantic StepTone (muted, success, failure, awaiting review/checks/user/workflow, in queue, plain), not a rich style. The active tones say what the step is waiting on, so no color distinction is lost and no style hint is needed. display.py maps tone to style in one _TONE_STYLES table.
  • AutolandStep.view(stack, *, is_next, assumed_completed) is a new abstract method, implemented on LandStep, WorkflowStep, and ConfirmStep. Display computes the two plan-position facts it takes (the pointer, and is_assumed_completed), so steps never see the plan or the pointer logic.
  • The per-type tables move into the step modules: STATE_LABELS/STATE_STYLES/_PR_STATE_OUTCOMES go to steps/land.py, and _WORKFLOW_STEP_STATUS goes to steps/workflow.py. _step_status, _step_target, _step_detail, _STEP_KINDS (now step.label), _land_entry, and the type branches of _describe_step and status._step_json are gone. _describe_step remains as a one-line wrapper that returns view().description.
  • display.py keeps all presentation: row layout, glyphs and icons, column widths, rich versus plain rendering, styles, markup escaping, joining detail fragments with the glyph separator, the headline, and the next-steps lines. status.py builds each step's JSON from generic fields plus view.data. Neither one branches on step type anymore, and the assert isinstance(step, ConfirmStep) shims from phase 1 are removed.
  • PRState moves to a new autoland/pr_state.py, because steps/land.py needs it at runtime and model.py imports steps. model re-exports it, so no imports change.

No user-visible change. Console output, status text, and status JSON are byte-for-byte identical, and the order of JSON keys is unchanged.

Test plan

  • Golden autoland scenarios (tests/test_autoland_golden.py) pass unchanged, with no golden files regenerated
  • New view tests in tests/test_autoland_steps.py pin the StepView for each step type: land across every PRState, with error/retry detail, untitled, and already landed; workflow across every state, plus its error detail; confirm when pending, awaiting confirmation, confirmed, and assumed completed, plus with no condition
  • The registered-step-type coverage now also requires every type to be concrete and to return a StepView with a status, key, and description
  • 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 2 of the autoland step-abstraction stack. Each step type now says what it has to show, and `display.py` decides how it looks. Before, `display.py` and `status.py` switched on the step's type to pick its status label, color, subject, detail line, prose description, and JSON fields.

- New `steps/view.py` holds the presentation-neutral contract. `StepView` is a frozen dataclass with these fields: `outcome` (a `StepOutcome`: done / active / pending / failed, moved from display's `_Outcome` with the same values), `status` ("Waiting for checks", "Assumed completed", ...), `tone`, `key` ("modular#103", "deploy.yaml", the condition or "(no condition)"), `description` (the one-line prose used in "Next steps" and in replan warnings), `note` (the PR title), `detail` (a tuple of fragments for the second line), and `data` (the type-specific `--status -o json` fields).
- `tone` is a semantic `StepTone` (muted, success, failure, awaiting review/checks/user/workflow, in queue, plain), not a rich style. The active tones say what the step is waiting on, so no color distinction is lost and no style hint is needed. `display.py` maps tone to style in one `_TONE_STYLES` table.
- `AutolandStep.view(stack, *, is_next, assumed_completed)` is a new abstract method, implemented on `LandStep`, `WorkflowStep`, and `ConfirmStep`. Display computes the two plan-position facts it takes (the pointer, and `is_assumed_completed`), so steps never see the plan or the pointer logic.
- The per-type tables move into the step modules: `STATE_LABELS`/`STATE_STYLES`/`_PR_STATE_OUTCOMES` go to `steps/land.py`, and `_WORKFLOW_STEP_STATUS` goes to `steps/workflow.py`. `_step_status`, `_step_target`, `_step_detail`, `_STEP_KINDS` (now `step.label`), `_land_entry`, and the type branches of `_describe_step` and `status._step_json` are gone. `_describe_step` remains as a one-line wrapper that returns `view().description`.
- `display.py` keeps all presentation: row layout, glyphs and icons, column widths, rich versus plain rendering, styles, markup escaping, joining detail fragments with the glyph separator, the headline, and the next-steps lines. `status.py` builds each step's JSON from generic fields plus `view.data`. Neither one branches on step type anymore, and the `assert isinstance(step, ConfirmStep)` shims from phase 1 are removed.
- `PRState` moves to a new `autoland/pr_state.py`, because `steps/land.py` needs it at runtime and `model.py` imports `steps`. `model` re-exports it, so no imports change.

No user-visible change. Console output, status text, and status JSON are byte-for-byte identical, and the order of JSON keys is unchanged.

## Test plan

- [x] Golden autoland scenarios (`tests/test_autoland_golden.py`) pass unchanged, with no golden files regenerated
- [x] New view tests in `tests/test_autoland_steps.py` pin the `StepView` for each step type: land across every `PRState`, with error/retry detail, untitled, and already landed; workflow across every state, plus its error detail; confirm when pending, awaiting confirmation, confirmed, and assumed completed, plus with no condition
- [x] The registered-step-type coverage now also requires every type to be concrete and to return a `StepView` with a status, key, and description
- [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: #70, branch: micahjsmith/stack/4
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