From f3b3ebad9b28a5342d6d7702a0f0bd353662bf38 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Thu, 4 Jun 2026 01:00:01 -0700 Subject: [PATCH] fix(portability): Windows aipass init + drone stdout (DPLAN-0194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 28 ++++++++++++++++++--- src/aipass/aipass/apps/modules/init_flow.py | 20 ++++++++++++--- src/aipass/drone/cli.py | 23 ++++++++++++++++- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f79888..d806c6d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/aipass/aipass/apps/modules/init_flow.py b/src/aipass/aipass/apps/modules/init_flow.py index 52cbaa44..2077aac4 100644 --- a/src/aipass/aipass/apps/modules/init_flow.py +++ b/src/aipass/aipass/apps/modules/init_flow.py @@ -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." diff --git a/src/aipass/drone/cli.py b/src/aipass/drone/cli.py index b403da0f..78331765 100644 --- a/src/aipass/drone/cli.py +++ b/src/aipass/drone/cli.py @@ -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