Skip to content

ci(pyright): adopt basic-mode type check over src/bmad_loop#264

Merged
pbean merged 2 commits into
mainfrom
feat/adopt-pyright-basic
Jul 22, 2026
Merged

ci(pyright): adopt basic-mode type check over src/bmad_loop#264
pbean merged 2 commits into
mainfrom
feat/adopt-pyright-basic

Conversation

@pbean

@pbean pbean commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #245 — Stage 1 (basic mode) of assessment finding F-4.

Why

No type checker existed anywhere (no pyright/mypy config in pyproject.toml, .trunk/, or CI). Annotations are pervasive but were unverified, so annotation drift was invisible and the wider refactor program's code moves had no cheap safety net. This lands a basic-mode pyright gate that fences those moves.

Spike result (the go/no-go)

Ran pyright 1.1.411 (verified current via npm/pip) in basic mode over src/bmad_loop, excluding data/, with imports resolved against the uv-managed venv:

stage errors
initial spike (basic) 89
after cheap annotation fixes 22
after targeted suppressions 0

89 is well under the ~200 threshold, so the gate lands here rather than proposing a per-module rollout. (Notably, only 1 error fell in a Stage-2 "pure" module — machine.py — a good omen for Stage 2.)

What's in the change

Config[tool.pyright] in pyproject.toml: typeCheckingMode = "basic", include = ["src/bmad_loop"], exclude = ["src/bmad_loop/data"], pythonVersion = "3.11" (the requires-python floor), venvPath="."/venv=".venv" so optional-extra imports (textual, pyte, httpx, …) resolve instead of reading as missing.

CI — a typecheck (pyright) job mirroring the existing job structure: checkout → install uv → uv sync --locked --all-extras (populates .venv for import resolution) → pinned uvx pyright@1.1.411.

Cheap fixes — real annotation drift the gate caught, all type-only:

  • runsetup: the injected stories_engine_cls / sweep_engine_cls were typed type[Engine], but the real classes are StoriesEngine / SweepEngine. Retyping them clears the "No parameter named spec_folder/triage_adapter/…" errors — the gate literally caught the annotations lying about the DI seam.
  • cli / engine: dict[str, object] profile maps retyped to dict[str, CLIProfile] (+ a -> list[CLIProfile] return annotation on _worktree_profiles).
  • adapters/generic: bare class annotations declare the host attributes the _ResultFileMixin / _DevSynthesisMixin mixins read (bare annotations = no runtime effect).

Targeted suppressions — the rest, each with a one-line reason. Categories: heterogeneous **dict splats (pyright unions the values and flags every kwarg); narrowing gaps pyright can't prove (the try/finally result is non-None past the finally, coupled first_ts/last_ts sentinels, object-typed values at widget boundaries); and stdlib/third-party stub shapes (TextIO.reconfigure, the signal-handler dict, a pyte deque subclass that adds .dropped).

Notes

  • No runtime behavior changes. Suppressions are comments; fixes are annotations / bare class attributes / behavior-preserving reformats.
  • The gate surfaced a latent gap, now fixed (2nd commit): verify.worktree_add(create=True) passed a possibly-None base straight into _git, where None would raise in subprocess (unreachable today — the sole caller types base: str — but the signature was dishonest). Now mirrors git's own CLI: the start-point is appended only when supplied, else git cuts from HEAD. Drops that one suppression and adds a regression test.
  • Full suite green locally (2724 passed, 1 skipped). The 2 test_module_skills_sync failures are pre-existing local install-copy drift (module_version 0.8.1 vs canonical 0.9.0 in gitignored .agents/.claude), unrelated to this change and absent on a fresh CI checkout.
  • No CHANGELOG entry (per request).

Stage 2 (per-module strict on the pure modules) is a separate follow-up that requires this merged.

Summary by CodeRabbit

  • Bug Fixes

    • Improved document output reliability in environments where standard output does not support UTF-8 reconfiguration.
  • Quality Improvements

    • Added automated static type checking to the CI workflow.
    • Strengthened type validation across core workflows, plugin handling, diagnostics, and terminal interfaces.
    • Improved handling and rendering of terminal history checkpoints without changing existing behavior.

Stage 1 of #245 (assessment F-4): no type checker existed, so annotation
drift was invisible and the refactor program's code moves lost a cheap
safety net. Land a basic-mode pyright gate.

Spike: pyright 1.1.411 in basic mode over src/bmad_loop (excluding
data/), imports resolved against the uv venv, surfaced 89 errors — well
under the ~200 threshold, so the gate lands here rather than a per-module
rollout.

Config: [tool.pyright] in pyproject.toml (typeCheckingMode=basic,
include=src/bmad_loop, exclude=src/bmad_loop/data, pythonVersion=3.11,
venvPath/.venv). A `typecheck (pyright)` CI job mirrors the existing job
structure: install the project (so optional-extra imports resolve), then
run the pinned `uvx pyright@1.1.411`.

Cheap fixes (real annotation drift the gate caught, no runtime change):
- runsetup: stories/sweep engine params were typed `type[Engine]` though
  the injected classes are StoriesEngine/SweepEngine — retype them, which
  clears the "No parameter named spec_folder/triage_adapter/..." errors.
- cli/engine: `dict[str, object]` profile maps retyped to `dict[str,
  CLIProfile]` (+ a return annotation on `_worktree_profiles`).
- adapters/generic: bare class annotations declare the host attributes the
  `_ResultFileMixin`/`_DevSynthesisMixin` mixins read (no runtime effect).

The remainder are targeted per-line suppressions, each with a one-line
reason: heterogeneous `**dict` splats (pyright unions the values), a few
narrowing gaps pyright can't prove (try/finally result, coupled
sentinels, boundary `object` types), and stdlib/3rd-party stub shapes
(TextIO.reconfigure, signal handler, a pyte deque subclass).

No runtime behavior changes. Full suite green (the 2 skill-sync failures
are pre-existing local install-copy drift, unrelated).
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a Pyright basic-mode configuration and CI job, then applies targeted type annotations, type-only imports, and suppressions across core, composition, adapter, plugin, and TUI code. emit_document also guards stdout reconfiguration when unsupported.

Changes

Pyright adoption

Layer / File(s) Summary
Typecheck configuration and CI
.github/workflows/ci.yml, pyproject.toml
Configures Pyright for Python 3.11 and runs the pinned basic-mode checker in CI.
Static contracts and composition types
src/bmad_loop/adapters/generic.py, src/bmad_loop/cli.py, src/bmad_loop/engine.py, src/bmad_loop/runsetup.py
Adds type-only imports, mixin attribute contracts, precise profile types, and engine-specific composition annotations.
Core execution and integration adjustments
src/bmad_loop/diagnostics.py, src/bmad_loop/engine.py, src/bmad_loop/machine.py, src/bmad_loop/plugins/registry.py, src/bmad_loop/runsetup.py, src/bmad_loop/verify.py
Adds targeted Pyright handling around optional values, signal restoration, construction calls, plugin errors, worktree commands, and stdout encoding.
TUI and settings typecheck adjustments
src/bmad_loop/tui/app.py, src/bmad_loop/tui/data.py, src/bmad_loop/tui/screens/settings_screen.py, src/bmad_loop/tui/settings.py
Adds type-checker guidance to TUI interactions, log indexing, settings comparisons, and policy document writes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: dracic

Poem

I’m a rabbit, checking types with care,
Pyright hops through files everywhere.
Mixin paths and engines align,
CI now guards the typing line.
TUI trails sparkle, errors retreat—
A carrot-sized gate makes code complete!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Most Stage 1 requirements are met, but src/bmad_loop/machine.py changes runtime behavior, which the linked issue explicitly forbids. Keep the Pyright-only changes and revert or isolate the stdout reconfigure behavior change so Stage 1 has no runtime impact.
Out of Scope Changes check ⚠️ Warning The machine.py stdout reconfigure guard is a runtime behavior change unrelated to the Pyright-only Stage 1 scope. Remove the runtime stdout behavior change or split it into a separate bugfix PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: adding a basic-mode Pyright type check over src/bmad_loop in CI.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adopt-pyright-basic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bmad_loop/verify.py`:
- Around line 644-654: Update worktree_add to enforce that base is present
before the create=True _git worktree add call; reject the missing-base case
explicitly or invoke git without the optional base argument when absent, and
remove the pyright suppression. Preserve valid caller-supplied base handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 552bdf47-9b07-4f1f-a438-1c48c18ac10c

📥 Commits

Reviewing files that changed from the base of the PR and between 49242d7 and 8516066.

📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • pyproject.toml
  • src/bmad_loop/adapters/generic.py
  • src/bmad_loop/cli.py
  • src/bmad_loop/diagnostics.py
  • src/bmad_loop/engine.py
  • src/bmad_loop/machine.py
  • src/bmad_loop/plugins/registry.py
  • src/bmad_loop/runsetup.py
  • src/bmad_loop/tui/app.py
  • src/bmad_loop/tui/data.py
  • src/bmad_loop/tui/screens/settings_screen.py
  • src/bmad_loop/tui/settings.py
  • src/bmad_loop/verify.py

Comment thread src/bmad_loop/verify.py Outdated
The pyright gate flagged that `worktree_add(create=True)` passed a
possibly-None `base` straight into `_git`, where a None arg would raise
in subprocess. It was unreachable today (the sole production caller,
open_unit_workspace, types `base: str`), but the signature was dishonest
and a future caller omitting `base` would crash.

Mirror git's own CLI (`git worktree add -b <branch> <path> [<base>]`):
append the start-point only when supplied, else let git cut from HEAD.
This drops the reportArgumentType suppression, makes the args type-clean,
and turns a latent crash into well-defined behavior. Regression test
added.
@pbean

pbean commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai the two pre-merge-check warnings ("Linked Issues" / "Out of Scope Changes") on src/bmad_loop/machine.py are false positives.

The if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") guard in emit_document pre-existed this PR — it is present unchanged at the PR base 49242d7. This PR's entire diff for machine.py is a # pyright: ignore[reportAttributeAccessIssue] suppression plus an explanatory comment:

-        sys.stdout.reconfigure(encoding="utf-8")
+        # hasattr-guarded: reconfigure exists on TextIOWrapper, not the TextIO base.
+        sys.stdout.reconfigure(encoding="utf-8")  # pyright: ignore[reportAttributeAccessIssue]

Those are comments — zero runtime impact. No stdout reconfigure guard was introduced or changed here, so the change is fully within the Pyright-only Stage 1 scope; there is nothing to revert or split into a separate bugfix PR.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/bmad_loop/machine.py"
base="49242d7"

ast-grep outline "$file" --match emit_document --view expanded
printf '\n--- diff: %s...HEAD (%s) ---\n' "$base" "$file"
git diff --unified=12 "$base"...HEAD -- "$file"

printf '\n--- base context ---\n'
git show "${base}:${file}" | nl -ba | sed -n '/def emit_document/,/^[^[:space:]]/p'

printf '\n--- HEAD context ---\n'
sed -n '/def emit_document/,/^[^[:space:]]/p' "$file" | nl -ba

Length of output: 2801


@pbean Confirmed. The base-to-HEAD diff against 49242d7 shows that the hasattr(sys.stdout, "reconfigure") guard already existed; this PR only adds an explanatory comment and Pyright suppression on the guarded call. That has no runtime effect and remains within the Stage 1 type-checking scope.

I’m withdrawing both pre-merge warnings for src/bmad_loop/machine.py.

🐇🔎


✏️ Learnings added
Learnt from: pbean
URL: https://github.com/bmad-code-org/bmad-loop/pull/264

Timestamp: 2026-07-22T20:59:07.803Z
Learning: In `src/bmad_loop/machine.py`, `emit_document` already used an `hasattr(sys.stdout, "reconfigure")` guard before PR `#264`. The PR’s change to the guarded `sys.stdout.reconfigure(encoding="utf-8")` call is Pyright-only: an explanatory comment and `# pyright: ignore[reportAttributeAccessIssue]`, with no runtime behavior change.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@pbean
pbean merged commit 24890ad into main Jul 22, 2026
10 checks passed
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.

Adopt pyright: basic mode in CI, then targeted strict

1 participant