fix(portability): Windows aipass init + drone stdout (DPLAN-0194)
Two latent Windows portability bugs caught by the new e2e wiring harness:
- aipass init: _preflight_check ancestor walk crashed on OSError from
un-enumerable Windows drive-root entries (pagefile.sys), swallowed by
route_command as 'Unknown command: init'. Walk now skips unreadable
entries (logged).
- drone @branch: crashed with UnicodeEncodeError ('charmap') printing a
routed branch's captured output via Rich on cp1252 stdout. PYTHONUTF8
only affects child interpreters; entry point now reconfigure()s the live
stdout/stderr to UTF-8.
Pure portability — Linux/macOS behaviour unchanged. e2e suite 14/14 green
locally on Linux. Lets the 3-OS CI verify Windows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cd1af34be8
commit
f3b3ebad9b
+24
-4
@@ -18,10 +18,12 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format
|
||||
install + console scripts (T0), `aipass init` scaffolding (T1), a hook actually
|
||||
firing via the bridge with an observable `engine.jsonl` record (T2a), and
|
||||
`drone` resolving + subprocess-executing a real branch (T3). Runs on a 3-OS
|
||||
matrix (ubuntu/windows/macos, `fail-fast: false`). Validated green on Linux;
|
||||
Windows is **red-first by design** — the expected failures (unguarded `.venv`
|
||||
symlink, `.venv/bin` vs `Scripts`, hardcoded `/tmp`) are the portability
|
||||
fix-list, not regressions. (DPLAN-0194 / FPLAN-0239)
|
||||
matrix (ubuntu/windows/macos, `fail-fast: false`). Ran red-first on Windows by
|
||||
design and immediately earned its keep — it caught two real, *previously
|
||||
uncovered* Windows wiring bugs (`aipass init` preflight + `drone` stdout
|
||||
encoding, both fixed below). Notably the layers we most feared — clean-wheel
|
||||
install (T0) and hook firing (T2a) — passed on Windows. (DPLAN-0194 /
|
||||
FPLAN-0239)
|
||||
- **`drone rm` — provider-agnostic safe delete** — a contained recursive delete
|
||||
that lets agents clean up scratch dirs without tripping the `rm -rf` block.
|
||||
Deletes are confined to the project root and the system temp dirs (`/tmp` and
|
||||
@@ -62,6 +64,24 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Two latent Windows portability bugs caught by the new e2e harness** — both
|
||||
were always present in the code; they only surfaced now because this is the
|
||||
first CI to run `aipass init` scaffolding and real-branch `drone` routing on
|
||||
Windows (the old Windows CI ran an editable install, `aipass`-less, and only
|
||||
routed to in-process modules, so both paths had zero Windows coverage). Pure
|
||||
portability fixes — Linux/macOS behaviour is unchanged.
|
||||
- **`aipass init` failed on Windows with a misleading "Unknown command: init".**
|
||||
`_preflight_check` walks every ancestor of CWD up to the filesystem root
|
||||
calling `iterdir()`/`is_file()`; at the Windows drive root a locked system
|
||||
entry (e.g. `pagefile.sys`) raises `OSError`, which the CLI's `route_command`
|
||||
swallowed and mislabeled. The ancestor walk now skips un-enumerable /
|
||||
un-stattable entries (logged), so init proceeds normally.
|
||||
- **`drone @branch` crashed on Windows with `UnicodeEncodeError ('charmap')`.**
|
||||
`drone` resolved + subprocessed the branch correctly, then crashed *printing*
|
||||
the captured output: Rich wrote Unicode through a cp1252 stdout. The existing
|
||||
`PYTHONUTF8` guard only affected child interpreters, not the live process
|
||||
streams — `drone`'s entry point now also `reconfigure()`s stdout/stderr to
|
||||
UTF-8 in place. (DPLAN-0194)
|
||||
- **A release merge can no longer destroy the `dev` branch** — `drone @git merge`
|
||||
passed `--delete-branch` to `gh pr merge` unconditionally, so merging a
|
||||
`dev`→`main` PR deleted the persistent `dev` branch on the remote and stranded
|
||||
|
||||
@@ -690,10 +690,24 @@ def _preflight_check() -> str | None:
|
||||
"This directory is an agent branch (has .trinity/passport.json).\n"
|
||||
"Agents are managed by 'drone @spawn', not 'aipass init'."
|
||||
)
|
||||
# Block if inside an existing AIPass project (registry above us)
|
||||
# Block if inside an existing AIPass project (registry above us).
|
||||
# Walking up to the filesystem root can hit ancestors that can't be
|
||||
# enumerated or stat'd — e.g. locked Windows system entries at the drive
|
||||
# root (pagefile.sys) raise OSError. Skip those rather than crash; on
|
||||
# POSIX everything up to / is readable so behaviour is unchanged there.
|
||||
for parent in [cwd] + list(cwd.parents):
|
||||
for f in parent.iterdir():
|
||||
if f.is_file() and f.name.endswith("_REGISTRY.json"):
|
||||
try:
|
||||
entries = list(parent.iterdir())
|
||||
except OSError as exc:
|
||||
logger.info("[init_flow] skipping unreadable ancestor %s: %s", parent, exc)
|
||||
entries = []
|
||||
for f in entries:
|
||||
try:
|
||||
is_registry = f.is_file() and f.name.endswith("_REGISTRY.json")
|
||||
except OSError as exc:
|
||||
logger.info("[init_flow] skipping unstattable entry %s: %s", f, exc)
|
||||
continue
|
||||
if is_registry:
|
||||
return (
|
||||
f"Already inside an AIPass project (found {f.name} at {parent}).\n"
|
||||
"Use 'aipass init update' to upgrade an existing project."
|
||||
|
||||
+22
-1
@@ -1,3 +1,11 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: cli.py
|
||||
# Description: Drone CLI entry point — console_scripts wrapper
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-05
|
||||
# Modified: 2026-06-04
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Drone CLI — command-line interface for aipass.drone.
|
||||
|
||||
@@ -19,8 +27,21 @@ import sys
|
||||
# Windows terminals default to cp1252 which can't encode Rich's Unicode
|
||||
# characters (box-drawing, em dashes, arrows). Force UTF-8 before any
|
||||
# imports that trigger Rich output.
|
||||
#
|
||||
# PYTHONUTF8 only affects *child* interpreters launched afterward — it does
|
||||
# nothing for this process's already-open stdout/stderr, which were created
|
||||
# with the cp1252 codec at interpreter startup. Rich writes through those
|
||||
# live streams, so we must reconfigure them in place (Python 3.7+). Without
|
||||
# this, `drone @branch` crashes with UnicodeEncodeError ('charmap') when it
|
||||
# prints a routed branch's captured output on Windows.
|
||||
if sys.platform == "win32":
|
||||
os.environ.setdefault("PYTHONUTF8", "1")
|
||||
os.environ.setdefault("PYTHONUTF8", "1") # for child subprocesses
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
# getattr guard: streams replaced by a capture layer (e.g. pytest) or
|
||||
# not backed by a TextIOWrapper simply lack reconfigure — skip them.
|
||||
_reconfigure = getattr(_stream, "reconfigure", None)
|
||||
if _reconfigure is not None:
|
||||
_reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
from aipass.drone.apps.drone import main as _drone_main
|
||||
|
||||
|
||||
Reference in New Issue
Block a user